ui
ui copied to clipboard
Example: Combobox with create
https://ui.shadcn.com/docs/components/combobox is great, but having an option for Create: ... either pinned to the top or bottom would be great too!
Behavior like https://react-select.com/home#creatable
This can be done with existing command items. If I have time later I can try to contribute it! glancing at the structure of the docs site I think I understand how I can add it
I have created it as you described, maybe it looks similar. You can try this
'use client';
import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
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 ComboboxOptions = {
value: string;
label: string;
};
type Mode = 'single' | 'multiple';
interface ComboboxProps {
mode?: Mode;
options: ComboboxOptions[];
selected: string | string[]; // Updated to handle multiple selections
className?: string;
placeholder?: string;
onChange?: (event: string | string[]) => void; // Updated to handle multiple selections
onCreate?: (value: string) => void;
}
export function Combobox({
options,
selected,
className,
placeholder,
mode = 'single',
onChange,
onCreate,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState<string>('');
return (
<div className={cn('block', className)}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
key={'combobox-trigger'}
type='button'
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
>
{selected && selected.length > 0 ? (
<div className='relative mr-auto flex flex-grow flex-wrap items-center overflow-hidden'>
<span>
{mode === 'multiple' && Array.isArray(selected)
? selected
.map(
(selectedValue: string) =>
options.find((item) => item.value === selectedValue)
?.label
)
.join(', ')
: mode === 'single' &&
options.find((item) => item.value === selected)?.label}
</span>
</div>
) : (
placeholder ?? 'Select Item...'
)}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-72 max-w-sm p-0'>
<Command
filter={(value, search) => {
if (value.includes(search)) return 1;
return 0;
}}
// shouldFilter={true}
>
<CommandInput
placeholder={placeholder ?? 'Cari Item...'}
value={query}
onValueChange={(value: string) => setQuery(value)}
/>
<CommandEmpty
onClick={() => {
if (onCreate) {
onCreate(query);
setQuery('');
}
}}
className='flex cursor-pointer items-center justify-center gap-1 italic'
>
<p>Create: </p>
<p className='block max-w-48 truncate font-semibold text-primary'>
{query}
</p>
</CommandEmpty>
<ScrollArea>
<div className='max-h-80'>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.label}
value={option.label}
onSelect={(currentValue) => {
if (onChange) {
if (mode === 'multiple' && Array.isArray(selected)) {
onChange(
selected.includes(option.value)
? selected.filter(
(item) => item !== option.value
)
: [...selected, option.value]
);
} else {
onChange(option.value);
}
}
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selected.includes(option.value)
? 'opacity-100'
: 'opacity-0'
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</div>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
</div>
);
}
and use like this
<Combobox
mode='single' //single or multiple
options={yourOptions}
placeholder='Select option...'
selected={typeSelected} // string or array
onChange={(value) => console.log(value)}
onCreate={(value) => {
handleCreateOptions(value);
}}
/>
I would really like this component as well
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 have created it as you described, maybe it looks similar. You can try this
'use client'; import * as React from 'react'; import { Check, ChevronsUpDown } from 'lucide-react'; 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 ComboboxOptions = { value: string; label: string; }; type Mode = 'single' | 'multiple'; interface ComboboxProps { mode?: Mode; options: ComboboxOptions[]; selected: string | string[]; // Updated to handle multiple selections className?: string; placeholder?: string; onChange?: (event: string | string[]) => void; // Updated to handle multiple selections onCreate?: (value: string) => void; } export function Combobox({ options, selected, className, placeholder, mode = 'single', onChange, onCreate, }: ComboboxProps) { const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState<string>(''); return ( <div className={cn('block', className)}> <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button key={'combobox-trigger'} type='button' variant='outline' role='combobox' aria-expanded={open} className='w-full justify-between' > {selected && selected.length > 0 ? ( <div className='relative mr-auto flex flex-grow flex-wrap items-center overflow-hidden'> <span> {mode === 'multiple' && Array.isArray(selected) ? selected .map( (selectedValue: string) => options.find((item) => item.value === selectedValue) ?.label ) .join(', ') : mode === 'single' && options.find((item) => item.value === selected)?.label} </span> </div> ) : ( placeholder ?? 'Select Item...' )} <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' /> </Button> </PopoverTrigger> <PopoverContent className='w-72 max-w-sm p-0'> <Command filter={(value, search) => { if (value.includes(search)) return 1; return 0; }} // shouldFilter={true} > <CommandInput placeholder={placeholder ?? 'Cari Item...'} value={query} onValueChange={(value: string) => setQuery(value)} /> <CommandEmpty onClick={() => { if (onCreate) { onCreate(query); setQuery(''); } }} className='flex cursor-pointer items-center justify-center gap-1 italic' > <p>Create: </p> <p className='block max-w-48 truncate font-semibold text-primary'> {query} </p> </CommandEmpty> <ScrollArea> <div className='max-h-80'> <CommandGroup> {options.map((option) => ( <CommandItem key={option.label} value={option.label} onSelect={(currentValue) => { if (onChange) { if (mode === 'multiple' && Array.isArray(selected)) { onChange( selected.includes(option.value) ? selected.filter( (item) => item !== option.value ) : [...selected, option.value] ); } else { onChange(option.value); } } }} > <Check className={cn( 'mr-2 h-4 w-4', selected.includes(option.value) ? 'opacity-100' : 'opacity-0' )} /> {option.label} </CommandItem> ))} </CommandGroup> </div> </ScrollArea> </Command> </PopoverContent> </Popover> </div> ); }and use like this
<Combobox mode='single' //single or multiple options={yourOptions} placeholder='Select option...' selected={typeSelected} // string or array onChange={(value) => console.log(value)} onCreate={(value) => { handleCreateOptions(value); }} />
You should create a Gist with the code you've come up with 😁 or maybe even make a PR to add to the documentation! I think it could be useful for a lot of people (like me, for example) 🔥🔥🔥.
🤓 But be careful, at the time of writing (20/03/2024), you'll need to downgrade the cmdk package to v0.2.0.
To avoid downgrade cmdk to v0.2.0 you need to wrap map iteration with CommandList
https://github.com/shadcn-ui/ui/issues/2980#issuecomment-2026578564
<CommandGroup>
<CommandList>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
onSelect(currentValue);
}}
>
<CheckCircledIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
{option.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
Originally posted by @LogicalOgbonna in https://github.com/shadcn-ui/ui/issues/2980#issuecomment-2026578564
We also have to replace data-[disabled] with data-[disabled:'true'] in CommandItem component to enable items.
https://github.com/shadcn-ui/ui/issues/2944#issuecomment-1986982481
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-base outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className
)}
{...props}
/>
));
note the change data-[disabled='true']
Originally posted by @abolajibisiriyu in https://github.com/shadcn-ui/ui/issues/2944#issuecomment-1986982481