Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting MUI's Autocomplete to correctly display categories and subcategories

I'm trying to essentially achieve the following image which is found here:

MUI Autocomplete

In that thread, they talk about the best way to display categories and subcategories and the consensus is an MUI Autocomplete.

I'm not however sure how I would achieve something like that at all and would like some help with how I could achieve it.

What I need is for the user to only be able to select one category, whether it be a "root category" or a sub-category. So in the example above, either the "Boysenberry" or the "Brulee Berry".

I also want to try and have the id of said category so I can apply it on my back end (which I'm sure I can do.

My fetched json structure looks like the below:

[
    {
        "id": 1,
        "name": "Audio Visual Equipment",
        "parent": null,
        "stockItems": [],
        "childCategories": [
            {
                "id": 2,
                "name": "Projectors",
                "stockItems": [],
                "childCategories": [
                    {
                        "id": 3,
                        "name": "Lenses",
                        "stockItems": [],
                        "childCategories": []
                    }
                ]
            }
        ]
    },
    {
        "id": 4,
        "name": "Lighting Equipment",
        "parent": null,
        "stockItems": [],
        "childCategories": [
            {
                "id": 5,
                "name": "Intelligent",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 6,
                "name": "Generic",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 7,
                "name": "Control",
                "stockItems": [],
                "childCategories": []
            }
        ]
    },
    {
        "id": 8,
        "name": "Sound Equipment",
        "parent": null,
        "stockItems": [],
        "childCategories": [
            {
                "id": 9,
                "name": "Mixing Desk",
                "stockItems": [],
                "childCategories": []
            }
        ]
    },
    {
        "id": 10,
        "name": "Cables",
        "parent": null,
        "stockItems": [],
        "childCategories": [
            {
                "id": 11,
                "name": "Multicore",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 12,
                "name": "Lighting",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 13,
                "name": "Audio",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 14,
                "name": "Video",
                "stockItems": [],
                "childCategories": []
            },
            {
                "id": 15,
                "name": "Power",
                "stockItems": [],
                "childCategories": []
            }
        ]
    }
]

EDIT:-

I get the following warning when I refresh the page:

MUI: The value provided to Autocomplete is invalid.None of the options match with `-1`.You can use the `isOptionEqualToValue` prop to customize the equality test. 

When I then click on the Autocomplete, I get the "root" categories only. When I then click on one, the name is not shown and I get the following error:

MUI: The value provided to Autocomplete is invalid.None of the options match with `1`.You can use the `isOptionEqualToValue` prop to customize the equality test. 
like image 382
JamieRhys Avatar asked Dec 06 '25 17:12

JamieRhys


1 Answers

1. Flattening the List

My approach is to "flatten" the list of categories into a single array so that MUI can evaluate each sub-category. Each of my flat options has a depth property so that I can display it with the correct level of indentation.

We can use the code from the Checkboxes example and add an indentation with the MUI sx prop:

renderOption={(props, option, { selected }) => (
  <li {...props}>
    <Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
    {option.name}
  </li>
)}

enter image description here

2. Filtering Matches

I'm assuming that we want to display the top-level category above a sub-category which matches on the sub-category term only. Like in your linked "ber" example, if the category was "Fall Gold" and the subcategory was "Fall Gold Berry". This means that we should consider the child terms when deciding if a term is a match.

To achieve this, I am including a matchTerms property on all option objects and using a custom filterOptions function on the Autocomplete which looks at this property. With the createFilterOptions utility, we just need to determine what texts to examine:

filterOptions={(createFilterOptions({
  // join with some arbitrary separator to prevent matches across adjacent terms
  stringify: (option) => option.matchTerms.join("//")
}))}

enter image description here

3. Highlighting

The last piece of this is the highlighting, which is not included in MUI. The MUI docs recommend the autosuggest-highlight package and include an example of how to use it. We can copy that, changing option.title to option.name. enter image description here

Complete Code

JavaScript

import {
  Autocomplete,
  TextField,
  Checkbox,
  createFilterOptions
} from "@mui/material";
import { data } from "./data";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";

const toOptions = (category, depth = 0, parentId = null) => {
  const { id, name, childCategories = [] } = category;
  const children = childCategories.flatMap((child) =>
    toOptions(child, depth + 1, id)
  );
  const option = {
    id,
    name,
    depth,
    parentId,
    matchTerms: [name].concat(children.map((obj) => obj.name))
  };
  return [option].concat(children);
};

const optionsList = data.flatMap((category) => toOptions(category));

export default () => {
  return (
    <Autocomplete
      options={optionsList}
      getOptionLabel={(option) => option.name}
      renderOption={(props, option, { selected, inputValue }) => {
        const matches = match(option.name, inputValue);
        const parts = parse(option.name, matches);
        return (
          <li {...props}>
            <Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
            <div>
              {parts.map((part, index) => (
                <span
                  key={index}
                  style={{
                    fontWeight: part.highlight ? 700 : 400
                  }}
                >
                  {part.text}
                </span>
              ))}
            </div>
          </li>
        );
      }}
      renderInput={(params) => <TextField {...params} />}
      filterOptions={createFilterOptions({
        // join with some arbitrary separator to prevent matches across adjacent terms
        stringify: (option) => option.matchTerms.join("//")
      })}
    />
  );
};

TypeScript

import {
  Autocomplete,
  TextField,
  Checkbox,
  createFilterOptions
} from "@mui/material";
import { data } from "./data";
import parse from "autosuggest-highlight/parse";
import match from "autosuggest-highlight/match";

// describes the input data
type Category = {
  id: number;
  name: string;
  childCategories?: Category[];
};

// describes the format that we want
interface Option {
  id: number;
  name: string;
  depth: number;
  parentId: number | null;
  matchTerms: string[];
}

const toOptions = (
  category: Category,
  depth: number = 0,
  parentId: number | null = null
): Option[] => {
  const { id, name, childCategories = [] } = category;
  const children = childCategories.flatMap((child) =>
    toOptions(child, depth + 1, id)
  );
  const option = {
    id,
    name,
    depth,
    parentId,
    matchTerms: [name].concat(children.map((obj) => obj.name))
  };
  return [option].concat(children);
};

const optionsList: Option[] = data.flatMap((category) => toOptions(category));

export default () => {
  return (
    <Autocomplete
      options={optionsList}
      getOptionLabel={(option) => option.name}
      renderOption={(props, option, { selected, inputValue }) => {
        const matches = match(option.name, inputValue);
        const parts = parse(option.name, matches);
        return (
          <li {...props}>
            <Checkbox checked={selected} sx={{ ml: 2 * option.depth }} />
            <div>
              {parts.map((part, index) => (
                <span
                  key={index}
                  style={{
                    fontWeight: part.highlight ? 700 : 400
                  }}
                >
                  {part.text}
                </span>
              ))}
            </div>
          </li>
        );
      }}
      renderInput={(params) => <TextField {...params} />}
      filterOptions={createFilterOptions({
        // join with some arbitrary separator to prevent matches across adjacent terms
        stringify: (option) => option.matchTerms.join("//")
      })}
    />
  );
};

CodeSandbox Link

like image 152
Linda Paiste Avatar answered Dec 08 '25 07:12

Linda Paiste



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!