DEV Community

nj145
nj145

Posted on • Edited on

React - Multi-select/deselect selection input using MUI Autocomplete

Introduction

Autocomplete is a popular UI component used in web applications to enhance user experience and productivity. It allows users to quickly search and select from a large set of options.
Material UI (MUI) provides an AutoComplete component which is very useful especially if you need to search and select multiple options.

https://mui.com/material-ui/react-autocomplete/#multiple-values
Here's what MUI offers us out of the box.

But, the MUI Autocomplete has some limitations as well. Suppose, I want to do a select all from a large list, it is a tedious task to keep clicking and selecting. Also, there is no way to select/deselect all of the filtered items from the list easily.

So, this is the exact problem I had to solve. I wanted to filter and select from a very large list of items and also provide the ability to select/deselect all options or select/deselect all of the filtered options.

MUI Autocomplete

Suppose we have a list of Departments under different Companies and we need a way to either select/deselect them all or filter by a particular Department and select all filtered options.

I've come up with a custom AutoComplete to solve this very problem which uses MUI Autocomplete and some javascript.

Tools and Dependencies:

  • React
  • Material UI
  • lodash

Link to GitHub Repo - https://github.com/nj145/multiselectDeselect-dropdown

Open in CodeSandbox - https://codesandbox.io/s/github/nj145/multiselectDeselect-dropdown

Code Explained

  • We have 2 components:
    • Selection
    • MultiSelectAll

Selection component

import React, { useState } from "react";
import { Grid, TextField, Typography } from "@mui/material";
import MultiSelectAll from "./MultiSelectAll";

const Selection = () => {
  const initialValue = [{ label: "Uber/Sales", value: "14" }];

  const departmentNames = [
    { label: "Amazon/Sales", value: "1" },
    { label: "Amazon/Customer Support", value: "2" },
    { label: "Google/Operations", value: "3" },
    { label: "Google/Engineering", value: "4" },
    { label: "Google/Services", value: "5" },
    { label: "Google/Customer Support", value: "6" },
    { label: "Netflix/Sales", value: "7" },
    { label: "Netflix/Engineering", value: "8" },
    { label: "Netflix/Services", value: "9" },
    { label: "Netflix/Operations", value: "10" },
    { label: "Microsoft/Sales", value: "11" },
    { label: "Microsoft/Operations", value: "12" },
    { label: "Microsoft/Customer Support", value: "13" },
    { label: "Uber/Sales", value: "14" },
    { label: "Uber/Services", value: "15" },
    { label: "Uber/Customer Support", value: "16" },
    { label: "Uber/Engineering", value: "17" },
    { label: "Uber/Operations", value: "18" },
    { label: "Walmart/Sales", value: "19" },
    { label: "Walmart/Services", value: "20" },
    { label: "Walmart/Operations", value: "21" },
    { label: "Walmart/Engineering", value: "22" },
    { label: "Walmart/Customer Support", value: "23" },
    { label: "CVS/Sales", value: "24" },
    { label: "CVS/Customer Support", value: "25" },
    { label: "CVS/Engineering", value: "26" },
    { label: "CVS/Operations", value: "27" }
  ];

  const getTextBoxInputValue = (input) => {
    return input.map((itm) => itm.label).join(";");
  };

  const [currentSelection, setCurrentSelection] = useState(
    getTextBoxInputValue(initialValue)
  );

  const handleSelectionChange = (result) => {
    const valueToSave = result.map((itm) => itm.label).join(";");
    setCurrentSelection(valueToSave);
  };

  return (
    <Grid container flexDirection="column" alignItems="center">
      <Grid item xs={12} sx={{ p: 2 }}>
        <MultiSelectAll
          sx={{ maxheight: "700px" }}
          items={departmentNames}
          selectAllLabel="Select All"
          value={initialValue}
          onChange={handleSelectionChange}
        />
      </Grid>
      <Grid item xs={12} sx={{ p: 2 }}>
        <Typography>Selected items:</Typography>
        <TextField sx={{ width: "450px" }} value={currentSelection} />
      </Grid>
    </Grid>
  );
};

export default Selection;

Enter fullscreen mode Exit fullscreen mode
  • This component displays the MultiSelectAll component and a simple text box that lists all the selected values separated by a semi colon.
  • The MultiSelectAll component takes in an array of options in the form of label-value pairs and renders them as a dropdown menu. It also displays a "Select All" option, allowing users to select all the available options with a single click.
  • When a user selects or deselects an option, the handleSelectionChange function is called. This function takes in the updated selection and updates the state of the currentSelection variable by converting the selected options' labels into a semicolon-separated string.

MultiSelectAll Component

import React, { useState, useEffect, useRef } from "react";
import isEqual from "lodash/isEqual";
import debounce from "lodash/debounce";

import {
  Autocomplete,
  Checkbox,
  Grid,
  FormControlLabel,
  TextField
} from "@mui/material";
import { createFilterOptions } from "@mui/material/Autocomplete";

const MultiSelectAll = ({ items, selectAllLabel, onChange, value }) => {
  const [selectedOptions, setSelectedOptions] = useState(value);
  const [filteredOptions, setFilteredOptions] = useState(null);
  const multiSelectRef = useRef(null);

  useEffect(() => {
    onChange(selectedOptions);
  }, [selectedOptions]);

  const handleToggleOption = (selectedOptions) =>
    setSelectedOptions(selectedOptions);
  const handleClearOptions = () => setSelectedOptions([]);
  const getOptionLabel = (option) => `${option.label}`;

  const allItemsSelected = () => {
    // if options are filtered, check to see if all filtered options are in selected items
    // if yes, selectAll - true, else selectAll - false
    // if options are not filtered, check to see if all items are selected or not
    if (filteredOptions?.length !== items.length) {
      const excludedFilteredOptions = filteredOptions?.filter(
        (opt) => !selectedOptions.find((selOpt) => selOpt.label === opt.label)
      );
      if (excludedFilteredOptions?.length > 0) {
        return false;
      }
      return true;
    }
    const allSelected =
      items.length > 0 && items.length === selectedOptions.length;
    return allSelected;
  };

  const clearSelected = (selOptions) => {
    // filter out the selOptions
    if (selOptions.length > 0) {
      setSelectedOptions(
        selectedOptions.filter(
          (item) =>
            !selOptions.find((selOption) => selOption.label === item.label)
        )
      );
    } else {
      setSelectedOptions([]);
    }
  };

  const handleSelectAll = (isSelected) => {
    let selectedList = [];
    if (
      filteredOptions?.length > 0 &&
      filteredOptions.length !== items.length
    ) {
      selectedList = items.filter((item) =>
        filteredOptions.find(
          (filteredOption) => filteredOption.label === item.label
        )
      );
    }
    if (isSelected) {
      if (selectedList.length > 0) {
        setSelectedOptions([...selectedOptions, ...selectedList]);
      } else {
        setSelectedOptions(items);
      }
    } else {
      clearSelected(selectedList);
    }
  };

  const handleToggleSelectAll = () => {
    handleSelectAll(!allItemsSelected());
  };

  const handleChange = (event, selectedOptions, reason) => {
    let result = null;
    if (reason === "clear") {
      handleClearOptions();
    } else if (reason === "selectOption" || reason === "removeOption") {
      if (selectedOptions.find((option) => option.value === "select-all")) {
        handleToggleSelectAll();
        // let result = [];
        result = items.filter((el) => el.value !== "select-all");
        // onChange(result);
      } else {
        handleToggleOption(selectedOptions);
        result = selectedOptions;
        // onChange(selectedOptions);
      }
    }
  };

  const handleCheckboxChange = (e, option) => {
    if (option.value === "select-all") {
      handleToggleSelectAll();
      // if (e.target.checked) {
      //     // onChange(items);
      // } else {
      //     // onChange([]);
      // }
    } else if (e.target.checked) {
      const result = [...selectedOptions, option];
      setSelectedOptions(result);
      // onChange(result);
    } else {
      const result = selectedOptions.filter(
        (selOption) => selOption.value !== option.value
      );
      setSelectedOptions(result);
      // onChange(result);
    }
  };

  const optionRenderer = (props, option, { selected }) => {
    const selectAllProps =
      option.value === "select-all" // To control the state of 'select-all' checkbox
        ? { checked: allItemsSelected() }
        : {};
    return (
      <Grid container key={option.label}>
        <Grid item xs={12} sx={{ pl: 1, pr: 1 }}>
          <FormControlLabel
            control={
              <Checkbox
                key={option.label}
                checked={selected}
                onChange={(e) => handleCheckboxChange(e, option)}
                {...selectAllProps}
                sx={{ mr: 1 }}
              />
            }
            label={getOptionLabel(option)}
            key={option.label}
          />
        </Grid>
      </Grid>
    );
  };

  const debouncedStateValue = debounce((newVal) => {
    // console.log(isEqual(newVal, filteredOptions));
    if (newVal && !isEqual(newVal, filteredOptions)) {
      // console.log('setting filtered options');
      setFilteredOptions(newVal);
    }
  }, 1000);

  const updateFilteredOptions = (filtered) => {
    debouncedStateValue(filtered);
  };

  const inputRenderer = (params) => <TextField {...params} />;

  const filter = createFilterOptions();

  return (
    <Autocomplete
      ref={multiSelectRef}
      sx={{
        width: "350px",
        maxHeight: "120px",
        overflowY: "scroll"
      }}
      multiple
      size="small"
      options={items}
      value={selectedOptions}
      disableCloseOnSelect
      getOptionLabel={getOptionLabel}
      isOptionEqualToValue={(option, val) => option.value === val.value}
      filterOptions={(options, params) => {
        const filtered = filter(options, params);
        updateFilteredOptions(filtered);
        return [{ label: selectAllLabel, value: "select-all" }, ...filtered];
      }}
      onChange={handleChange}
      renderOption={optionRenderer}
      renderInput={inputRenderer}
    />
  );
};

export default MultiSelectAll;

Enter fullscreen mode Exit fullscreen mode
  • The MultiSelectAll component takes in an array of items to be displayed, a label for the "Select All" option, a function to handle changes, and a value to represent the selected items.
  • The allItemsSelected function determines if all options are selected or not.
  • The handleToggleSelectAll function toggles the selection of all options when the "Select All" checkbox is checked or unchecked.
  • The handleChange function handles changes to the selected options. If the "Select All" option is checked or unchecked, the handleToggleSelectAll function is called. Otherwise, the handleToggleOption function is called.
  • The handleCheckboxChange function handles the change of an individual checkbox. If the "Select All" checkbox is checked, the handleToggleSelectAll function is called. Otherwise, the selected item is added or removed from the selected options list.
  • The optionRenderer function is a helper function that renders each option in the dropdown.
  • The Autocomplete component is used to render the multi-select dropdown. It uses the filterOptions function to filter the options based on user input. The getOptionLabel function is used to get the label of an option. The isOptionEqualToValue function is used to check if two options are equal. The renderInput function is used to render the input field.

Hope this helped! :)

Top comments (2)

Collapse
 
hoangvuvan0611 profile image
Vũ Văn Hoàng

Great!!!!

Collapse
 
abhigunjan profile image
Abhishek Gunjan

Thanks, It was useful.