Material UI: Autocomplete with value set as string and options set as objects?

Viewed 1346

I'm trying to create a custom Select component that uses material ui's Autocomplete as a base.

However, I'm running into issues trying to set the value to a string like how normal select dropdowns are usually used. It seems to only work if I set the value to the whole object.

App.js

const options = [
  { id: '1', name: 'United States' },
  { id: '2', name: 'Canada' },
  { id: '3', name: 'Mexico' },
];

const App = () => {
  const [val, setVal] = useState();

  return (
    <Select
      options={options}
      value={val}             //<----- only works if val === one of the country objects in options array, but i want to se it as country.id
      label="Country"
      placeholder="Enter a country"
      onChange={newValue => setVal(newValue)}
      getOptionLabel={(option) => option.name}
      getOptionValue={(option) => option.id}
    />
  );
}

Select.js:

const Select = ({
  getOptionLabel,
  getOptionValue,
  placeholder,
  options,
  label,
  onChange,
  value = ""
}) => {
  const compareOptionAndValue = (option, value) => {
    console.log("oooooooooo", option);
    console.log("vvvvvvvvvvv", value);
    console.log("eeeeeeee", getOptionValue(option) === value);

    return getOptionValue(option) === value;
  };

  return (
    <MuiAutocomplete
      options={options}
      value={value}
      getOptionSelected={compareOptionAndValue}
      getOptionLabel={(option) =>
        option && getOptionLabel(option) ? getOptionLabel(option) : ""
      }
      onChange={(_event, option) => onChange(getOptionValue(option))}
      renderInput={(params) => (
        <MuiTextField
          {...params}
          label={label}
          placeholder={value ? undefined : placeholder}
          variant="outlined"
          fullWidth
        />
      )}
    />
  );
};

getOptionSelected doesn't work as expected. I thought it would help determine which option was selected but it doesn't do anything even though it returns true.

Here's a codesandbox example:

https://codesandbox.io/s/material-ui-autocomplete-forked-brjis?file=/src/index.js

1 Answers

Figured out a way. It's hacky but it works I guess...

Gonna just paste my whole component with the extra stuff:

const Select = React.forwardRef(
  (
    {
      getOptionLabel,
      getOptionValue,
      multiple,
      placeholder,
      options,
      label,
      error,
      disabled,
      onChange,
      value = multiple ? [] : '',
    },
    ref
  ) => {
    const renderCheckbox = (option, { selected }) => (
      <Checkbox checked={selected} label={getOptionLabel(option)} />
    );

    // 05/2021 - Material UI does not provide
    // access to the whole list item, only the contents
    const renderOption = (option) => (
      <OptionText>{getOptionLabel(option)}</OptionText>
    );

    // 05/2021 - Material UI does not work when the value
    // is of a different type than the options object. Therefore
    // this hack is needed to improve the API of our Select component
    const findMatchingOptions = ({ options, value, multiple }) =>
      multiple
        ? options.filter((option) =>
            value.some((choice) => getOptionValue(option) === choice)
          )
        : options.find((option) => getOptionValue(option) === value);

    const getDefaultValue = ({ multiple }) => (multiple ? [] : '');

    return (
      <SelectContainer
        ref={ref}
        popupIcon={<CaretDownIcon />}
        multiple={multiple}
        options={options}
        disableClearable
        disableCloseOnSelect={multiple}
        value={
          findMatchingOptions({ options, value, multiple }) ||
          getDefaultValue({ multiple })
        }
        getOptionLabel={(option) =>
          option && getOptionLabel(option) ? getOptionLabel(option) : ''
        }
        onChange={(_event, option) => {
          onChange(
            multiple ? option.map(getOptionValue) : getOptionValue(option)
          );
        }}
        disabled={disabled}
        renderInput={(params) => (
          <StyledTextField
            {...params}
            label={label}
            error={!!error}
            helperText={error}
            placeholder={value ? undefined : placeholder}
            variant="outlined"
            fullWidth
          />
        )}
        renderTags={(value) => {
          if (!multiple) {
            return <SelectedValueText>{value}</SelectedValueText>;
          }

          const num_tags = value.length;
          const first_choice = value[0];

          return (
            <SelectedValueText>
              {truncate(getOptionLabel(first_choice), { length: 25 })}
              {num_tags > 1 && ` +${num_tags - 1}`}
            </SelectedValueText>
          );
        }}
        renderOption={multiple ? renderCheckbox : renderOption}
      />
    );
  }
);
Related