Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Autocomplete] Option to select all options if multiple selections enabled #21211

Closed
1 task done
vileppanen opened this issue May 26, 2020 · 20 comments
Closed
1 task done
Labels
component: autocomplete This is the name of the generic UI component, not the React module! waiting for 👍 Waiting for upvotes

Comments

@vileppanen
Copy link
Contributor

vileppanen commented May 26, 2020

  • I have searched the issues of this repository and believe that this is not a duplicate.

Summary 💡

I tried to search from issues whether this would've been discussed already, but wasn't able to found one, so here goes:

Would it make sense to provide a prop for Autocomplete component to display a fixed "Select all" option in the options drop-down, if multiple is set for the component?

Examples 🌈

CodeSandbox demo, with ListBox customization:
https://codesandbox.io/s/thirsty-moon-9egd9

Motivation 🔦

The requirement I'd need to fulfill, is that every selectable option should be easily selected "via one click", and, the ability to do so should exist in close vicinity of the Autocomplete component (= in the options ListBox)

Currently, this needs customization from the developer, and seems that there are many ways to achieve the wanted behavior. But gut feeling is, that each customization is prone to contain some amount of smelling hacks (might also be, that I've missed some key factors upon implementations).

Some technical requirements, I came up with for the "select all" enabled Autocomplete:

  • separate the "select all" from other actual "options" (for me it seems clearer to have that distinction)
  • use the Autocomplete component, instead of useAutocomplete hook, as the Autocomplete component provides 90% of other functionalities out of the box (which is more than enough for me)
  • display "select all" always on top, outside of the scrolling content

I've experimented this, by providing a customized ListBox for the Autocomplete component, that always renders some "select all" component on top, and props.children after that.

This mostly works, but the "select all" cannot be focused via keyboard navigation, as it's missing the tabindex and the data-option-index attributes --> data-option-index is dynamically generated, and I wouldn't want to "regenerate" them. Why I'd need to regenerate them? Because I'd like to have the "select all" option not to be included in the options, and the attributes are generated for the options.

@oliviertassinari oliviertassinari added the component: autocomplete This is the name of the generic UI component, not the React module! label May 26, 2020
@oliviertassinari
Copy link
Member

@vileppanen Why not making the "Select all" action an option, like the other?

@vileppanen
Copy link
Contributor Author

vileppanen commented May 26, 2020

Initially I thought of it, and I guess it would work 🤔 ...but then I'd have to customize at least the following:

  • I'd need to filter out the "select all" option from the selected values (in addition, I'd need some way to reliably identify, which of the selected values is the "select all" option)
  • I'd need to style the "select all" option so, that it would always float on top of the drop down list

Those two considerations made me hesitate, as I'd want to have quite simple and clear API on the the component. Let's say for instance, if I pass an array of objects, with following structure { id: 'some id', title: 'Some title' } to options --> how do I include the "select all" option in that sort of list, without having too much of additional filtering and logic to distinguish the select-all option?

Guess my main issue is, that I tend to think that the "select all" option is only a visual UI component, instead of an option that is derived from some external data?

@oliviertassinari
Copy link
Member

I'm closing as we have no bandwidth to handle special cases like this one.

I have added the waiting for users upvotes tag. Please upvote this issue if you are interested in seeing it resolved. We will prioritize our effort based on the number of upvotes.

@oliviertassinari oliviertassinari added the waiting for 👍 Waiting for upvotes label May 26, 2020
@oliviertassinari
Copy link
Member

You can find an demo on how to repurpose the option in https://material-ui.com/components/autocomplete/#creatable.

@vileppanen
Copy link
Contributor Author

vileppanen commented May 26, 2020

I've somehow missed the filterOptions prop completely, which seems to indeed help me on this matter...although it doesn't free me from the additional logic needed to exclude the "select-all" option.

....and just figured, in my case it's just the same to include the "select all" in the original options list :D...

But it'll do.

@flora8984461
Copy link

I've somehow missed the filterOptions prop completely, which seems to indeed help me on this matter...although it doesn't free me from the additional logic needed to exclude the "select-all" option.

....and just figured, in my case it's just the same to include the "select all" in the original options list :D...

But it'll do.

Hi, thank you for your opening this, I am also encountering the same issue. How you solve it when selecting all, the other selections are all selected?

Thanks for your help!

@vileppanen
Copy link
Contributor Author

vileppanen commented Jun 2, 2020

I've somehow missed the filterOptions prop completely, which seems to indeed help me on this matter...although it doesn't free me from the additional logic needed to exclude the "select-all" option.
....and just figured, in my case it's just the same to include the "select all" in the original options list :D...
But it'll do.

Hi, thank you for your opening this, I am also encountering the same issue. How you solve it when selecting all, the other selections are all selected?

Thanks for your help!

Not quite sure if it was this that you asked, but what I basically ended up with was something like this:

  1. I provide an array of objects with fixed structure to options, structure being { label: 'Some option label', value: 'the-value-of-the-option' }
  2. In filterOptions I just return the filtered values, accomodating following object on top of the list: { label: 'Select all', value: 'select-all' } (so I'm deciding here, that the value of 'select-all' will control the functionality, in the meantime I also lose the ability to pass in primitive string values as an array of options, because I cannot distinguish which values are real options generated elsewhere, and which one is the 'select-all' generated by the component itself...but this sacrifice wasn't too big to make)
  3. In onChange handler, I check explicitly, if the current selection of values contains select-all --> if it does, I execute the logic I need when "selecting all" (in other words --> I eventually set all options to be rendered in the value array)

@flora8984461
Copy link

flora8984461 commented Jun 2, 2020

I've somehow missed the filterOptions prop completely, which seems to indeed help me on this matter...although it doesn't free me from the additional logic needed to exclude the "select-all" option.
....and just figured, in my case it's just the same to include the "select all" in the original options list :D...
But it'll do.

Hi, thank you for your opening this, I am also encountering the same issue. How you solve it when selecting all, the other selections are all selected?
Thanks for your help!

Not quite sure if it was this that you asked, but what I basically ended up with was something like this:

  1. I provide an array of objects with fixed structure to options, structure being { label: 'Some option label', value: 'the-value-of-the-option' }
  2. In filterOptions I just return the filtered values, accomodating following object on top of the list: { label: 'Select all', value: 'select-all' } (so I'm deciding here, that the value of 'select-all' will control the functionality, in the meantime I also lose the ability to pass in primitive string values as an array of options, because I cannot distinguish which values are real options generated elsewhere, and which one is the 'select-all' generated by the component itself...but this sacrifice wasn't too big to make)
  3. In onChange handler, I check explicitly, if the current selection of values contains select-all --> if it does, I execute the logic I need when "selecting all" (in other words --> I eventually set all options to be rendered in the value array)

Thanks a lot for your quick response. I am wondering if the code below is like what you mentioned?

  const filterOptions = (options) => {
    return [{ label: 'Select all', value: 'Select all' }, ...options]
  }

And for handle onChange, I am using:

  const handleOnChange = (event, value) => {

    if ( value.indexOf({ label: 'Select all', value: 'Select all' }) ) {

      setValueSelected(options)   // I have const[valueSelected, setValueSelected]=useState([])

    }
    else {
      setValueSelected(value)
    }
  }

And I am using Autocomplete with props:

    <Autocomplete
      id="someId"
      multiple
      options={options}
      filterOptions={filterOptions}
      disableCloseOnSelect
      getOptionLabel={(option) => option.label}
      getOptionSelected={(option, value) => option.value=== value.value}
      onChange={handleOnChange}
      value={valueSelected}
      renderOption={(option, { selected }) => (
        <React.Fragment>
          <Checkbox
            icon={Icon}
            checkedIcon={CheckedIcon}
            style={{ marginRight: 8 }}
            checked={selected}
          />
          {option.label}
        </React.Fragment>
      )}
      renderInput={(params) => (
        <TextField {...params} variant="outlined" placeholder="sometext" />
      )}
    />

But I am also having an issue with when select All, it gives the warning:

The value provided to Autocomplete is invalid.
None of the options match with `{"label":"Select all","value":"Select all"}`.
You can use the `getOptionSelected` prop to customize the equality test.

While I am already using getOptionSelected and I also tried to replace option with filterOptions, but I still have this warning. Thus, I cannot go into my if logic in handleOnChange.

Am I doing something wrong? Or is this the sacrifice you mentioned?

I am also having a question that how can I make the checkbox also showing I am selecting all, and when I uncheck Select all, the items are all not selected?

Thanks again for your help.

@vileppanen
Copy link
Contributor Author

I revamped the demo in the initial issue description a bit to inhabit the usage of filterOptions (it lacks the "always on top floating" styling). Here's how I've setup the Autocomplete.

The filterOptions prop expects a function that takes (options, params) arguments --> the values need to be filtered by calling a function created by createFilterOptions before returning, like this:

const filter = createFilterOptions();
const filterOptions = (options, params) => {
   const filtered = filter(options, params);
   return [{ label: 'Select all', value: "Select all" }, ...filtered];
}

For the question how to control the "select-all" checkbox state, I just check it separately in the optionRenderer using the allSelected state variable, which is set in every render by comparing the selected values count agains available options count:

const optionRenderer = (option, { selected }) => {
    const selectAllProps =
      option.value === "select-all" // To control the state of 'select-all' checkbox
        ? { checked: allSelected }
        : {};
    return (
      <>
        <Checkbox
          color="primary"
          icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
          checkedIcon={<CheckBoxIcon fontSize="small" />}
          style={{ marginRight: 8 }}
          checked={selected}
          {...selectAllProps}
        />
        {getOptionLabel(option)}
      </>
    );
  };

I remember bumping few times to the same warning you describe, but in my case it was suppressed via getOptionSelected setup, similar as yours. Hard to say what could cause it without seeing the full setup 🤷

@flora8984461
Copy link

@vileppanen That works really nice. I was at wrong track, thank you very much.

@flora8984461
Copy link

I made some adjustmentsto make it more suitable for me to use. Thanks again for your help. And I made it to leave the dropdown empty when no options, since it keeps selecting "Select All" when no options.

My getOptionSelected is the same settings as yours. When making selections it won't give the warning, but when I submit the form, it still gives None of the options match with ... warning.
No idea what causes it.

@WilliamZimmermann
Copy link

WilliamZimmermann commented Jan 28, 2022

I could improve the Select All option getting off the "Select All" checkbox from the options list and putting it on the PopperComponent option. It's more clean. However, I got some problems and I would like a help to solve it and have a full functional component:

  1. When I scroll down and select (let's say) the last option on the list, the scroll up to the top;
  2. Let's suppose we "Select All". All works fine. We click outside the component to close the list. We click again to open the list. If now I try to to "Select All", the component interprets that I'm clicking out of the component and closes it...

Could anyone help me with it?
select-all

CODE SANDBOX LINK: https://codesandbox.io/s/checkboxestags-material-demo-forked-5b0pt

@rhinck
Copy link

rhinck commented May 12, 2022

@WilliamZimmermann Thanks so much for working on this solution, this has been great for my needs!

Were you ever able to figure out the solution to problem 1 that you mentioned? That's the only thing holding back the component from an awesome UX experience for me.

@wtek01
Copy link

wtek01 commented Jan 4, 2023

Hello,
Can you get a version with mui please.

Thans

@valerii15298
Copy link
Contributor

Hello, guys. I was able to solve it for myself without modifying options.
Specifically for this case I created a very small simple library mui-autocomplete-select-all (0.55 kb).
@rhinck also fixed the problem with the scroll.

Here is an example:

import { useState, useMemo } from "react";
import Checkbox from "@mui/material/Checkbox";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { MuiAutocompleteSelectAll } from "mui-autocomplete-select-all";

export function App() {
  const [value, setValue] = useState<string[]>([]);
  const options = useMemo(
    () => new Array(100).fill(0).map((_, i) => i.toString()),
    []
  );

  const selectedAll = value.length === options.length;
  return (
    <MuiAutocompleteSelectAll.Provider
      value={{
        onSelectAll: (selectedAll) => void setValue(selectedAll ? [] : options),
        selectedAll,
        indeterminate: !!value.length && !selectedAll,
      }}
    >
      <Autocomplete
        value={value}
        onChange={(_, newValue) => void setValue(newValue)}
        sx={{ width: 200, m: "auto" }}
        disableCloseOnSelect
        multiple
        limitTags={3}
        ListboxComponent={MuiAutocompleteSelectAll.ListBox}
        disablePortal
        options={options}
        renderInput={(params) => <TextField {...params} />}
        renderOption={(props, option, { selected }) => (
          <li key={option} {...props}>
            <Checkbox checked={selected} />
            {option}
          </li>
        )}
      />
    </MuiAutocompleteSelectAll.Provider>
  );
}

The implementation is actually very small using ListboxComponent and context.

image

@GauravBhagchandani
Copy link

I'm trying to do a select all as well, but I want to select all the filtered options not all the options in the dropdown. The only way I see how is either filter it myself in the onChange handler or save the filtered options from filterOptions into a separate state if that is possible. Has anyone done anything similar?

@ralvs
Copy link
Contributor

ralvs commented Nov 22, 2023

Hello guys! Just sharing the cleanest solution that I could come up with, and keeping all logic inside the Autocomplete component.

First thing, I mixed up the exemples of checkboxes and controlled states

And I got this:

export default function CheckboxesTags() {
  const [value, setValue] = React.useState([]);
  const [inputValue, setInputValue] = React.useState('');

  return (
    <Autocomplete
      multiple
      value={value}
      onChange={(event, newValue) => { setValue(newValue); }}
      options={top100Films}
      disableCloseOnSelect
      getOptionLabel={(option) => option.title}
      renderOption={(props, option, { selected }) => (
        <li {...props}>
          <Checkbox
            icon={icon}
            checkedIcon={checkedIcon}
            style={{ marginRight: 8 }}
            checked={selected}
          />
          {option.title}
        </li>
      )}
      style={{ width: 500 }}
      renderInput={(params) => (
        <TextField {...params} label="Checkboxes" placeholder="Favorites" />
      )}
    />
  );
}

Then I made 3 modifications: onChange, renderOption and add filterOptions.

export default function CheckboxesTags() {
  const [value, setValue] = React.useState([]);
  const [inputValue, setInputValue] = React.useState('');

  return (
    <Autocomplete
      multiple
      value={value}
      filterOptions={(options, params) => { // <<<--- inject the Select All option
        const filter = createFilterOptions()
        const filtered = filter(options, params)
        return [{ title: 'Select All...', all: true }, ...filtered]
      }}
      // onChange={(event, newValue) => { setValue(newValue); }} <<<--- OLD
      onChange={(event, newValue) => {
        if (newValue.find(option => option.all))
          return setValue(value.length === top100Films.length ? [] : top100Films)
  
          setValue(newValue)
      }}
      options={top100Films}
      disableCloseOnSelect
      getOptionLabel={(option) => option.title}
      renderOption={(props, option, { selected }) => (
        <li {...props}>
          <Checkbox
            icon={icon}
            checkedIcon={checkedIcon}
            style={{ marginRight: 8 }}
            // checked={selected} <<<--- OLD
            checked={option.all ? !!(value.length === top100Films.length) : selected}
          />
          {option.title}
        </li>
      )}
      style={{ width: 500 }}
      renderInput={(params) => (
        <TextField {...params} label="Checkboxes" placeholder="Favorites" />
      )}
    />
  );
}

Working demo here:
https://stackblitz.com/edit/react-grkosu?file=Demo.js

@dangkhoa99
Copy link

Hi all, I have an implementation of MUI Autocomplete like to excel.
Demo
image

@2598Nitz
Copy link

2598Nitz commented Jun 30, 2024

Hi all,

Bulk select operations is extremely slow when the number of options is 10k+ .
In my implementation, I am testing it out for 20k options and it takes 3 sec for select all.
Stackblitz editor link: https://stackblitz.com/edit/react-lyjfbf?file=Demo.js

Any leads in optimizing the performance is helpful, thanks !

@aress31
Copy link

aress31 commented Dec 17, 2024

This feature request has almost 100 👍 - maybe time to add it to the roadmap?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: autocomplete This is the name of the generic UI component, not the React module! waiting for 👍 Waiting for upvotes
Projects
None yet
Development

No branches or pull requests