Search code examples
javascriptreactjsecmascript-6material-uiformik

Material UI Autocomplete Categories in ReactJS


I'm using MUI Autocomplete and Formik and I wanted to group this into categories. If it has no sub_accounts, then it shouldn't have a header label. Something like this: https://mui.com/material-ui/react-autocomplete/#grouped

CODESANDBOX ------> CLICK HERE

Expected outcome on the UI is something like:

  • Petty Cash

  • Cash In Bank - Bank of America

    • Bank of America - Single Proprietor
    • Bank of America - Corporate Entity
  • Cash

  • CIB - Bank of Vietnam

    • Bank of Vietnam Personal
    • Bank of Vietnam Checking Acc

Petty Cash, Cash In Bank - Bank of America, Cash and CIB - Bank of Vietnam should be aligned.

Cash In Bank - Bank of America and CIB - Bank of Vietnam cannot be clicked/selected - only its sub_accounts can be selected as well as Petty Cash and Cash.

CODE

export const CashAccountAutocomplete = ({
  field,
  form: { touched, errors, setFieldValue, values },
  disabled,
  ...props
}) => {
  const [inputValue, setInputValue] = useState("");

  const handleChange = (_, newValue, reason) => {
    if (reason === "clear") {
      setFieldValue(field.name, { id: "", name: "" });
      return;
    }
    setFieldValue(field.name, newValue);
  };

  const handleInputChange = (_, newInputValue) => {
    setInputValue(newInputValue);
  };

  const extractSubAccounts = (accounts) => {
    if (!Array.isArray(accounts)) {
      console.error("Invalid accounts data. Expected an array.");
      return [];
    }

    return accounts.flatMap(
      ({ id, name, sub_accounts }) =>
        sub_accounts && sub_accounts.length > 0
          ? extractSubAccounts(sub_accounts) // Recursively extract sub-accounts
          : [{ id, name }] // Include the account if it has no sub-accounts
    );
  };

  const filteredData = extractSubAccounts(accounts);

  return (
    <Autocomplete
      {...field}
      disabled={disabled}
      getOptionLabel={(option) =>
        typeof option === "string" ? option : option?.name || ""
      }
      renderOption={(props, option) => {
        return (
          <li {...props} key={option.id}>
            {option?.name}
          </li>
        );
      }}
      filterOptions={(x) => x}
      options={filteredData || []}
      autoComplete
      includeInputInList
      filterSelectedOptions
      noOptionsText={"No data"}
      onChange={handleChange}
      inputValue={inputValue}
      onInputChange={handleInputChange}
      renderInput={(params) => (
        <TextField
          {...params}
          {...props}
          error={touched[field.name] && errors[field.name] ? true : false}
          helperText={
            touched[field.name] &&
            errors[field.name] &&
            String(errors[field.name].id)
          }
        />
      )}
      fullWidth
    />
  );
};

Solution

  • Update the extractSubAccounts helper function to only set the isHeader property on elements are will be headers, and then also set an isSelectable property if there are no sub-array options.

    const extractSubAccounts = (accounts) => {
      if (!Array.isArray(accounts)) {
        console.error("Invalid accounts data. Expected an array.");
        return [];
      }
    
      return accounts.flatMap(({ name, sub_accounts, ...account }) => [
        {
          ...account,
          name,
          isHeader: true,
          isSelectable: !sub_accounts?.length,
        },
        ...sub_accounts,
      ]);
    };
    

    Update the renderOptions callback to check both each list item's inHeader and isSelectable properties, the idea being that the list items that are headers and also options should be selectable and receive the "MuiAutocomplete-option" CSS classes and then also conditionally undo the padding that options have so the headers remain aligned.

    renderOption={(props, option) => {
      return (
        <li
          key={option.id}
          {...(!option.isHeader || option.isSelectable ? props : {})}
          style={{
            paddingLeft:
              option.isHeader || option.isSelectable ? "1rem" : "2rem",
          }}
        >
          {option.isHeader && !option.isSelectable
            ? <strong>{option.name}</strong>
            : option.name}
        </li>
      );
    }}
    

    enter image description here