Repository: h2oai/wave
File: ui/src/combobox.tsx (TypeScript)

Code before:
  label?: S
  /** A string that provides a brief hint to the user as to what kind of information is expected in the field. */
  placeholder?: S
  /** The name of the selected choice. */
  value?: S
  /** The names of the selected choices. If set, multiple selections will be allowed. */
  values?: S[]
  /** The choices to be presented. */
  choices?: S[]
  /** Text to be displayed as an error below the text box. */
  error?: S
  /** True if this field is disabled. */
  disabled?: B
  /** The width of the combobox, e.g. '100px'. Defaults to '100%'. */
  width?: S
  /** True if the component should be visible. Defaults to True. */
  visible?: B
  /** An optional tooltip message displayed when a user clicks the help icon to the right of the component. */
  tooltip?: S
  /** True if the choice should be submitted when an item from the dropdown is selected or the textbox value changes. */
  trigger?: B
  /** True if this is a required field. Defaults to False. */
  required?: B
}

export const XCombobox = ({ model: m }: { model: Combobox }) => !m.values ? <ComboboxSingleSelect model={m} /> : <ComboboxMultiSelect model={m} />

const ComboboxSingleSelect = ({ model: m }: { model: Omit<Combobox, 'values'> }) => {
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S | null>(m.value ?? null),
    skipUseEffectForValue = React.useRef(false),
    onChange = (_e: React.FormEvent<IComboBox>, option?: IComboBoxOption, _index?: U, value?: S) => {
      if (!option && value) setOptions((prevOptions = []) => [...prevOptions, { key: value, text: value }])
      
      const v = option?.text || value || ''
      setSelected(v)
      
      wave.args[m.name] = v
      if (m.trigger) wave.push()

      // Hacky: Ensure that next dynamically change to value from Wave app will trigger the useEffect
      m.value = v
      // The line above will trigger the useEffect for change in m.value and we need to skip it
      skipUseEffectForValue.current = true
    }
  
  React.useEffect(() => {
    wave.args[m.name] = (selected && options.map(o => String(o.key)).includes(selected || '')) ? selected : null
  }, [m.name, m.value, options, selected])


Review comment (suggestion):
Negated conditions are a bit harder to read in general so let's flip it
```suggestion
export const XCombobox = ({ model: m }: { model: Combobox }) => m.values ? <ComboboxMultiSelect model={m} /> : <ComboboxSingleSelect model={m} />
```

Code after:
  label?: S
  /** A string that provides a brief hint to the user as to what kind of information is expected in the field. */
  placeholder?: S
  /** The name of the selected choice. */
  value?: S
  /** The names of the selected choices. If set, multiple selections will be allowed. */
  values?: S[]
  /** The choices to be presented. */
  choices?: S[]
  /** Text to be displayed as an error below the text box. */
  error?: S
  /** True if this field is disabled. */
  disabled?: B
  /** The width of the combobox, e.g. '100px'. Defaults to '100%'. */
  width?: S
  /** True if the component should be visible. Defaults to True. */
  visible?: B
  /** An optional tooltip message displayed when a user clicks the help icon to the right of the component. */
  tooltip?: S
  /** True if the choice should be submitted when an item from the dropdown is selected or the textbox value changes. */
  trigger?: B
  /** True if this is a required field. Defaults to False. */
  required?: B
}

export const XCombobox = ({ model: m }: { model: Combobox }) => m.values ? <ComboboxMultiSelect model={m} /> : <ComboboxSingleSelect model={m} />

const ComboboxSingleSelect = ({ model: m }: { model: Omit<Combobox, 'values'> }) => {
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S | null>(m.value ?? null),
    onChange = (_e: React.FormEvent<IComboBox>, option?: IComboBoxOption, _index?: U, value?: S) => {
      const v = (option && String(option.key)) || value || ''
      setSelected(v)

      // Hacky: Ensure that next "model.value" set from a Wave App will be different and trigger the second "useEffect"
      m.value = v

      wave.args[m.name] = v
      if (m.trigger) wave.push()
    }

  React.useEffect(() => { wave.args[m.name] = selected }, [m.name, selected])

  // Whenever a new "value" is set in a Wave App, set it as the current value and add it to options list if it's not included yet
  React.useEffect(() => {
    setSelected(m.value || null)
    if (m.value && !(m.choices || []).includes(m.value)) {
      setOptions((prevOptions = []) => [...prevOptions, { key: m.value!, text: m.value! }])
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps