[Feature Request] Autocomplete to work with multi-select and chips #4670
Replies: 11 comments
-
i wrote this code to handle it with Select input using "react-select" library and a similar design to NextUi: import { Chip, SelectionMode } from "@nextui-org/react"; type Item = { interface Props { const MultiSelectInputWithSearch = ({
const [isDropDownOpen, setIsDropDownOpen] = useState(false); useEffect(() => { const handleChange = useCallback( function deleteSelectedItemByValue(value: number) { function setValueOfSelections( const formatOptionLabel = ({ value, label, subHeader }: Item) => ( {label} {subHeader && ( {subHeader} )} ); const MultiValueContainer = (props: any) => ( const handleSelectAll = () => { return ( <label htmlFor={ SELECT-${elementKey} }className="flex flex-row justify-between" > {label} {!((selectedOptions as MultiValue)?.length === items.length) && ( Select All )} <Select key={ SELECT-${elementKey} }isClearable={true} onChange={handleChange} onMenuClose={() => setIsDropDownOpen(false)} // Close dropdown when focus is lost isMulti={selectionMode === "multiple"} isSearchable={haveSearch} isLoading={isLoading} required={isRequired} isDisabled={isDisabled} placeholder={placeholder} value={selectedOptions} options={items} formatOptionLabel={formatOptionLabel} getOptionLabel={(option) => option.label} getOptionValue={(option) => option.value.toString()} components={{ MultiValueContainer }} menuIsOpen={isDropDownOpen} // Control dropdown visibility onMenuOpen={() => { setIsDropDownOpen(true); }} className="nextui-select" classNamePrefix="nextui-select" /> ); }; export default MultiSelectInputWithSearch; and here is the CSS file: .nextui-select__single-value { .nextui-select__multi-value { .nextui-select__indicator { .nextui-select__dropdown-indicator { .nextui-select__menu { .nextui-select__option { .nextui-select__option--is-focused { .nextui-select__option--is-selected { /* Dark Mode */ .dark .nextui-select__single-value { .dark .nextui-select__indicator { .dark .nextui-select__dropdown-indicator { .dark .nextui-select__menu { .dark .nextui-select__option { .dark .nextui-select__option--is-focused { .dark .nextui-select__option--is-selected { |
Beta Was this translation helpful? Give feedback.
-
I have a simple version without external libraries that worked well for us, display selected items in 'use client'
import { useState } from 'react'
import { Autocomplete, ChipGroup } from '@ui/components'
export interface AutocompleteWithChipsProps {
items: string[]
label: string
}
export function AutocompleteWithChips({
items: defaultItem,
label,
}: AutocompleteWithChipsProps) {
const [items, setItems] = useState<string[]>(defaultItem)
const [selectedItems, setSelectedItems] = useState<string[]>([])
const [inputValue, setInputValue] = useState('')
const handleSelectItem = (item: string) => {
if (!selectedItems.includes(item)) {
item && setSelectedItems([...selectedItems, item])
} else {
setSelectedItems(selectedItems.filter((f) => f !== item))
}
setInputValue('')
}
const filteredItems = inputValue
? [
...items.filter((item) =>
item.toLowerCase().includes(inputValue.toLowerCase()),
),
`Add: ${inputValue}`,
]
: items
const handleRemoveItem = (item: string) => {
setSelectedItems(
selectedItems.filter((selectedItem) => selectedItem !== item),
)
}
return (
<Autocomplete
items={filteredItems}
label={label}
classNames={{
endContentWrapper: 'w-full',
}}
endContent={
<ChipGroup
className='w-full justify-end'
items={selectedItems.map((item) => ({
key: item,
label: item,
className: 'cursor-pointer',
variant: 'dot',
onClick: () => handleRemoveItem(item),
}))}
/>
}
onSelectionChange={(item) => {
if (item && typeof item === 'string') {
if (item.startsWith('Add: ')) {
if (!items.includes(inputValue)) {
setItems([...items, inputValue])
}
handleSelectItem(inputValue)
} else {
handleSelectItem(item)
}
}
}}
selectedKey={''}
inputValue={inputValue}
onInputChange={setInputValue}
/>
)
} |
Beta Was this translation helpful? Give feedback.
-
thanks! can you share the code of ChipGroup please |
Beta Was this translation helpful? Give feedback.
-
+1 Need this feature |
Beta Was this translation helpful? Give feedback.
-
Kindly share the ChipGroup code. |
Beta Was this translation helpful? Give feedback.
-
I have just implemented a new solution for us regarding multiselect with the option to search for the listed elements. Feel free to use and wish you a good coding 🤲🏼
|
Beta Was this translation helpful? Give feedback.
-
I end up use React-Select. I need to use Autocomplete with form. |
Beta Was this translation helpful? Give feedback.
-
Now that Autocomplete supports virtualization, I have decided to abandon react-select. I created an autocomplete component with chips inside to manage the elements, inspired just by react-select. I created it to manage the array passed externally using the primaryKey specified in props for the identifier, and a name attribute. The only little problem I couldn't solve is not moving the focus to the first element after you select an element. I hope someone finds it useful. import { useCallback, useEffect, useRef, useState } from "react";
import { Autocomplete, AutocompleteItem, Button, Chip, cn } from "@nextui-org/react";
import { IconCheck, IconX } from "@tabler/icons-react";
export const AutocompleteMultiple = ({ items = [], selectedKeys = [], onSelectionChange = () => null, primaryKey = "id", ...props }) => {
const [inputValue, setInputValue] = useState("");
const inputRef = useRef(null);
// Manage the selection of the items
const handleSelectItem = (id) => {
if (!id) return;
if (!selectedKeys.map((key) => key.toString()).includes(id.toString())) {
onSelectionChange([...selectedKeys, id]);
} else handleRemoveItem(id);
setInputValue("");
};
const filteredItems = inputValue ? items.filter((item) => item.name.toLowerCase().includes(inputValue.toLowerCase())) : items;
const handleRemoveItem = (id) => onSelectionChange(selectedKeys.filter((i) => i.toString() !== id.toString()));
// Manage the filled-within state for the label
const selectedKeysRef = useRef(selectedKeys);
const changeFilledWithin = useCallback((filledWithin) => {
const isFilledWithin = !!(filledWithin === "true" || inputRef?.current?.getAttribute("data-filled-within") === "true");
const filled = isFilledWithin || selectedKeysRef.current.length;
inputRef?.current?.parentElement?.parentElement?.parentElement?.setAttribute("data-filled-within", !!filled ? "true" : "false");
}, []);
useEffect(() => {
selectedKeysRef.current = selectedKeys;
changeFilledWithin();
}, [selectedKeys]);
useEffect(() => {
const handleMutation = (mutationsList) => {
for (let mutation of mutationsList)
if (mutation.type === "attributes" && mutation.attributeName === "data-filled-within")
changeFilledWithin(mutation?.target?.getAttribute("data-filled-within"));
};
if (inputRef.current) {
const observer = new MutationObserver(handleMutation);
observer.observe(inputRef.current, { attributes: true });
return () => observer.disconnect();
}
}, []);
return (
<Autocomplete
ref={inputRef}
classNames={{
base: "overflow-hidden",
endContentWrapper: "absolute top-[0.4px] right-3",
}}
startContent={selectedKeys.map((id) => (
<Chip
key={id.toString()}
classNames={{
base: "bg-white rounded-lg min-w-0",
content: "truncate",
}}
endContent={
<IconX className="rounded-full hover:bg-default/40 p-1 cursor-pointer size-5 mr-1" onClick={() => handleRemoveItem(id)} />
}
>
{items.find((item) => item[primaryKey].toString() === id.toString())?.name}
</Chip>
))}
endContent={
<Button
variant="light"
isIconOnly
size="sm"
className={cn("rounded-full opacity-0 group-data-[hover=true]:opacity-100 data-[hover=true]:bg-default/40", {
hidden: !selectedKeys.length,
})}
onPress={() => onSelectionChange([])}
>
<IconX className="size-4" />
</Button>
}
selectedKey={null}
isClearable={false}
onSelectionChange={(id) => handleSelectItem(id)}
inputValue={inputValue}
onInputChange={setInputValue}
inputProps={{
classNames: {
label: "mt-2.5 group-data-[filled-within=true]:translate-y-0 group-data-[filled-within=true]:mt-0",
inputWrapper: cn(" block", {
"min-h-8": selectedKeys.length === 0,
"h-auto": selectedKeys.length > 0,
}),
innerWrapper: cn("flex flex-wrap gap-1 h-auto max-w-[calc(100%-4rem)]", {
"mt-3 -ml-1.5": selectedKeys.length === 0,
"mt-6": selectedKeys.length > 0,
}),
input: "w-20 h-7",
},
}}
{...props}
>
{filteredItems.map((item) => (
<AutocompleteItem
key={item[primaryKey]}
textValue={item.name}
endContent={selectedKeys.map((key) => key.toString()).includes(item[primaryKey].toString()) && <IconCheck className="size-4" />}
>
{item.name}
</AutocompleteItem>
))}
</Autocomplete>
);
}; Example: |
Beta Was this translation helpful? Give feedback.
-
let's track here - #4684 |
Beta Was this translation helpful? Give feedback.
-
Is your feature request related to a problem? Please describe.
I like simpler UIs that functionally support complex use-cases. One such use case is the ability to create a searchable text field that "queues" up items for some request to complete later on. An example being selecting various users to delete or attaching/creating tags for a blog post.
Describe the solution you'd like
I would like for the Autocomplete component to allow adding the ability to select multiple entries and also render them as chips. Additionally it would be nice if it also had the ability to create items on the fly (referring to my earlier example - creating a tag you should be able to very easily without employing a hacky method be allowed to type an autocomplete value and hit a button in the dropdown saying "add")
Describe alternatives you've considered
Currently the only one that works for my use case is the Select component with the multi-select ability + using chips. This however does not allow for the ability to dynamically add an entry without employing a hacky solution that adds bulk to the UI.
Screenshots or Videos
Material UI currently supports this and here is an example:
Beta Was this translation helpful? Give feedback.
All reactions