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

Code before:
  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()
    }
  
  // eslint-disable-next-line react-hooks/exhaustive-deps
  React.useEffect(() => { wave.args[m.name] = selected }, [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
  }, [m.value])

  return (
    <Fluent.ComboBox
      data-test={m.name}
      label={m.label}
      placeholder={m.placeholder}
      options={options}
      required={m.required}
      selectedKey={selected}
      allowFreeform
      disabled={m.disabled}
      autoComplete="on"
      errorMessage={m.error}
      onChange={onChange}
    />
  )

Review comment (suggestion):
This should be called when name changes as well IMO.

```suggestion
  React.useEffect(() => { wave.args[m.name] = selected }, [m.name, selected])
```

Code after:
  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
  }, [m.value])

  return (
    <Fluent.ComboBox
      data-test={m.name}
      label={m.label}
      placeholder={m.placeholder}
      options={options}
      required={m.required}
      selectedKey={selected}
      allowFreeform
      disabled={m.disabled}
      autoComplete="on"
      errorMessage={m.error}
      onChange={onChange}
    />
  )
}