Implementation of the `Select all` functionality using react-select package
Introduction
This article will explain the implementation details of the Select all
functionality in the multi-select component based on the react-select v5.1.0 package.
Demo
handleChange function
The primary logic of the "Select all" option has been implemented in this function. There can be three main scenarios in the process:
- All the elements in the list are selected.
- Some of the options in the menu are selected
- None of the options is selected.
The first case happens under certain conditions: the current state of the Select all
option is unchecked, the length of the selected elements is greater than zero, meanwhile, either the Select all
option or all the options in the menu except the Select all
option are selected. If these conditions are met, then all the elements in the menu are checked.
In the second case, we again check if the length of the selected options is greater than zero, and neither the Select all
option nor all of the remaining options in the menu list are selected. If that is the case, then it means only some of the elements are selected.
The third case is the condition in which neither all the elements nor some of them are selected which happens when the Select all
option is set to the unchecked state. If you look at the code, you will see that only filtered options have been used. It is because the default value of filter input is an empty string which works perfectly in both cases.
const handleChange = (selected: Option[]) => {
if (
selected.length > 0 &&
!isAllSelected.current &&
(selected[selected.length - 1].value === selectAllOption.value ||
JSON.stringify(filteredOptions) ===
JSON.stringify(selected.sort(comparator)))
)
return props.onChange(
[
...(props.value ?? []),
...props.options.filter(
({ label }: Option) =>
label.toLowerCase().includes(filterInput?.toLowerCase()) &&
(props.value ?? []).filter((opt: Option) => opt.label === label)
.length === 0
)
].sort(comparator)
);
else if (
selected.length > 0 &&
selected[selected.length - 1].value !== selectAllOption.value &&
JSON.stringify(selected.sort(comparator)) !==
JSON.stringify(filteredOptions)
)
return props.onChange(selected);
else
return props.onChange([
...props.value?.filter(
({ label }: Option) =>
!label.toLowerCase().includes(filterInput?.toLowerCase())
)
]);
};
Custom Option component
By overriding the Option component, checkboxes are added to the options list, moreover, if some of the elements are checked, then the indeterminate state of the Select all
option is set to true
.
const Option = (props: any) => (
<components.Option {...props}>
{props.value === "*" &&
!isAllSelected.current &&
filteredSelectedOptions?.length > 0 ? (
<input
key={props.value}
type="checkbox"
ref={(input) => {
if (input) input.indeterminate = true;
}}
/>
) : (
<input
key={props.value}
type="checkbox"
checked={props.isSelected || isAllSelected.current}
onChange={() => {}}
/>
)}
<label style={{ marginLeft: "5px" }}>{props.label}</label>
</components.Option>
);
Custom Input component
This custom input component creates a dotted box around the search input and automatically sets the focus to the search input which is helpful when there are lots of selected options.
const Input = (props: any) => (
<>
{filterInput.length === 0 ? (
<components.Input autoFocus={props.selectProps.menuIsOpen} {...props}>
{props.children}
</components.Input>
) : (
<div style={{ border: "1px dotted gray" }}>
<components.Input autoFocus={props.selectProps.menuIsOpen} {...props}>
{props.children}
</components.Input>
</div>
)}
</>
);
Custom filter function
This custom function is used to keep the Select all
option out of the filtering process.
const customFilterOption = ({ value, label }: Option, input: string) =>
(value !== "*" && label.toLowerCase().includes(input.toLowerCase())) ||
(value === "*" && filteredOptions?.length > 0);
Custom onInputChange function
This function is used to get the filter input value and set it to an empty string on the menu close event.
const onInputChange = (
inputValue: string,
event: { action: InputAction }
) => {
if (event.action === "input-change") setFilterInput(inputValue);
else if (event.action === "menu-close" && filterInput !== "")
setFilterInput("");
};
Custom KeyDown function
This function prevents default action on the space bar button click if the filter input value is not an empty string.
const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
if (e.key === " " && !filterInput) e.preventDefault();
};
Handling state and label value of Select all
option
The value of isAllSelected
determines the state of the Select all
option. And the value of the selectAllLabel
determines the value of the Select all
option label.
isAllSelected.current =
JSON.stringify(filteredSelectedOptions) ===
JSON.stringify(filteredOptions);
if (filteredSelectedOptions?.length > 0) {
if (filteredSelectedOptions?.length === filteredOptions?.length)
selectAllLabel.current = `All (${filtereds also sus also suOptions.length}) selected`;
else
selectAllLabel.current = `${filteredSelectedOptions?.length} / ${filteredOptions.length} selected`;
} else selectAllLabel.current = "Select all";
selectAllOption.label = selectAllLabel.current;
What else
This custom multi-select component also provides custom single-select with the checkboxes near options.
Side Notes
If you have a large number of options, you can solve performance issues, by rendering only the items in the list that are currently visible which allows for efficiently rendering lists of any size. To do that you can override the MenuList
component by implementing react-window's FixedSizeList. For implementation details, you can look at this stack overflow answer.
In the end, this is my first tech blog as a junior frontend developer, so it may not be very well-written. I'd appreciate any feedback.