skip to Main Content

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
    />
  );
};

2

Answers


  1. I have implemented a solution here using hooks.
    You need to sort the options property value of Autocomplete after filtering.

    Login or Signup to reply.
  2. 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={({ className, ...props }, option) => {
      return (
        <li
          key={option.id}
          {...props}
          className={(!option.isHeader || option.isSelectable) && className}
          style={option.isHeader && option.isSelectable ? { padding: 0 } : {}}
        >
          {option.isHeader ? <strong>{option.name}</strong> : option.name}
        </li>
      );
    }}
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search