ui
ui copied to clipboard
We need <AutoComplete /> like MUI and other UI libraries.
Need of AutoComplete
Hi, you can build one with Combobox element. When you say AutoComplete you mean with server-side fetches right?
Here's mine for reference:
'use client';
import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { useDebouncedCallback } from 'use-debounce';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { ScrollArea } from './scroll-area';
export type ComboBoxItemType = {
value: string;
label: string;
};
type ComboboxProps = {
value?: string;
onSelect: (value: string | undefined) => void;
items: ComboBoxItemType[];
searchPlaceholder?: string;
noResultsMsg?: string;
selectItemMsg?: string;
className?: string;
unselect?: boolean;
unselectMsg?: string;
onSearchChange?: (e: string) => void;
disabled?: boolean;
};
const popOverStyles = {
width: 'var(--radix-popover-trigger-width)',
};
export function Combobox({
value,
onSelect,
items,
searchPlaceholder = 'Pesquisar item...',
noResultsMsg = 'Nenhum item encontrado',
selectItemMsg = 'Selecione um item',
className,
unselect = false,
unselectMsg = 'Nenhum',
onSearchChange,
disabled = false,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const handleOnSearchChange = useDebouncedCallback((e: string) => {
if (onSearchChange) {
onSearchChange(e);
}
}, 300);
return (
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger disabled={disabled} asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('justify-between', className)}
>
{value
? items.find((item) => item.value === value)?.label
: selectItemMsg}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
style={popOverStyles}
className="popover-content-width-same-as-its-trigger p-0"
>
<Command>
<CommandInput
placeholder={searchPlaceholder}
onValueChange={handleOnSearchChange}
/>
<ScrollArea className="max-h-[220px] overflow-auto">
<CommandEmpty>{noResultsMsg}</CommandEmpty>
<CommandGroup>
{unselect && (
<CommandItem
key="unselect"
value=""
onSelect={() => {
onSelect(undefined);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === '' ? 'opacity-100' : 'opacity-0',
)}
/>
{unselectMsg}
</CommandItem>
)}
{items.map((item) => (
<CommandItem
key={item.value}
value={item.label}
onSelect={(currentValue) => {
onSelect(
currentValue === item.label.toLowerCase()
? item.value
: '',
);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === item.value ? 'opacity-100' : 'opacity-0',
)}
/>
{item.label}
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
);
}
And then you can use it like this if you like: PS: In my case, I extracted to a component because I use it multiple forms...
'use client';
import { list } from '@/actions/clients';
import { toaster } from '@/components/form/toaster';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { ComboBoxItemType, Combobox } from '../ui/combobox';
import { Label } from '../ui/label';
type ClientAutocompleteProps = {
value?: string;
disabled?: boolean;
setClientId: Dispatch<SetStateAction<string | undefined>>;
required?: boolean;
};
export default function ClientAutocomplete({
value,
disabled,
setClientId,
required = false,
}: ClientAutocompleteProps) {
const [clients, setClients] = useState<ComboBoxItemType[]>([]);
const handleClientSearchChanged = async (value: string) => {
if (value === '' || value.length < 2) {
return;
}
const response = await list(1, value);
if (response.type === 'error') {
toaster.send(response);
return;
}
setClients(
response.data?.data.map((client) => ({
value: client.id,
label: client.nomeFantasia,
})) || [],
);
};
useEffect(() => {
if (value) {
handleClientSearchChanged(value);
}
}, [value]);
return (
<>
<Label required={required}>Cliente</Label>
<Combobox
disabled={disabled}
value={value}
items={clients}
onSelect={(value) => setClientId(value)}
selectItemMsg="Pesquise pelo cliente"
searchPlaceholder="Pesquisar cliente..."
onSearchChange={handleClientSearchChanged}
{...(!required && { unselect: true, unselectMsg: 'Sem cliente' })}
/>
</>
);
}
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.
I agree, including an AutoComplete component would be great to have. The problem with the ComboBox solution is that you can only use existing items and can't add new items via an input field.
Thanks for all the great work!
@dseipp You can make an option for like Add +whatever is in the text box
which updates as the user types. If the add option is selected it can happen in the handler.
I agree
Please add the autocomplete to shadcn
I suspect the timeline/existence of autocomplete in shadcn ui is closely related to this feature in radix https://github.com/radix-ui/primitives/issues/1342
A combobox and autocomplete are 2 different types of components. Maybe @shadcn assumes they are the same thing, and thats why autocomplete does not exist in shadcn ui.
Using examples from mantine ui: https://mantine.dev/core/autocomplete https://mantine.dev/core/combobox
This is not necessarily the case. They chose to split as two components, probably to reduce complexity, but there is only one Combobox aria pattern officialy: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
This pattern covers both autocomplete
and autosuggest
combobox types.
In our Combobox
, we have just a prop to allow the user "free typing" (not restricted only to select one/multiple value in the menu), which turns it into an "autosuggest": https://sparkui.vercel.app/?path=/docs/components-combobox--docs#custom-value-entry