ui
ui copied to clipboard
ComboBox search behavior
The search box is not performing a new search when you hit the backspace key. if I type "nexz" I get no results when I hit backspace and now have "nex" I still have no result even though "Next.js" should show.
It is using cmdk package under the hood which is not really well suited for a select. So the displayed behavior is most likely a cmdk bug.
I found a quick way to fix it.
The example passes only the label which cmdk will use to create a value property. There seems to be some inconsistency. If you pass the value directly as a property the behaviour is as you would expect it.
I was also struggling with the same issue, thanks for the fix @olsio
Duplicate of #1450 and the example is fixed in #1522 - please close as duplicate
As @olsio says, you can pass in the value to fix it
I notice that when I'm using different value and label as below
const options = [
{ value: 1, label: "one" },
{ value: 2, label: "two" }
}
CombBox only search for value, how to to search base on value and label?
I notice that when I'm using different value and label as below
const options = [ { value: 1, label: "one" }, { value: 2, label: "two" } }
CombBox only search for value, how to to search base on value and label?
this could be a workaround if you are looking to search by label 🤔
<CommandItem
key={option.value}
value={option.label}
onSelect={(currentValue) => {
const value = options.find(
(option) => option.label.toLowerCase() === currentValue,
)?.value;
setValue(value ?? "");
setOpen(false);
}}
>
{option.label}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
value === company.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
I notice that when I'm using different value and label as below
const options = [ { value: 1, label: "one" }, { value: 2, label: "two" } }
CombBox only search for value, how to to search base on value and label?
this could be a workaround if you are looking to search by label 🤔
<CommandItem key={option.value} value={option.label} onSelect={(currentValue) => { const value = options.find( (option) => option.label.toLowerCase() === currentValue, )?.value; setValue(value ?? ""); setOpen(false); }} > {option.label} <CheckIcon className={cn( "ml-auto h-4 w-4", value === company.value ? "opacity-100" : "opacity-0", )} /> </CommandItem>
primarily, it's ok. But we will have issues when two items with same label will be there. Need to have a better solution for this.
yaa so if i do this
value={option.name}
then suppose i have three people with the name Nimesh but their id is different then it only shows 1 Nimesh instead of 3
anyone facing this issue?
I notice that when I'm using different value and label as below
const options = [ { value: 1, label: "one" }, { value: 2, label: "two" } }
CombBox only search for value, how to to search base on value and label?
this could be a workaround if you are looking to search by label 🤔
<CommandItem key={option.value} value={option.label} onSelect={(currentValue) => { const value = options.find( (option) => option.label.toLowerCase() === currentValue, )?.value; setValue(value ?? ""); setOpen(false); }} > {option.label} <CheckIcon className={cn( "ml-auto h-4 w-4", value === company.value ? "opacity-100" : "opacity-0", )} /> </CommandItem>
primarily, it's ok. But we will have issues when two items with same label will be there. Need to have a better solution for this.
you can set the value like a string template value={${option.label} id=${option.id}
}
and then imagine you have an onSubmit function where you receive this data, you can simply get that value and do a split value?.split('id=')[1] to get it. With this approach you will be able to search by label and by id in the searchbox.
Combobox is the weakest component I've used for these reasons
This needs an update asap.
I notice that when I'm using different value and label as below
const options = [ { value: 1, label: "one" }, { value: 2, label: "two" } }
CombBox only search for value, how to to search base on value and label?
Use the custom filter option and combine value
and label
as value for the CommandItem(s).
<Command
filter={(value, search) => {
if (value.includes(search)) return 1;
return 0;
}}
>
<CommandItem value={`${option.value} ${option.label}`}>
https://github.com/pacocoursey/cmdk/tree/v1.0.0?tab=readme-ov-file#parts-and-styling
Thank you @shomyx you both helped me fix the cmdk update to v1 and the search :)
It might be a bit more convenient to explicitly pass the keywords to the command item, so you don't have to parse the value again:
<CommandItem
key={item.value}
value={item.value}
keyords={[item.label]}
>
{item.label}
</CommandItem>
Depending on the data, I mostly found it more useful to normalise the search term and the haystack to lower case:
<Command
filter={(value, search, keywords = []) => {
const extendValue = value + " " + keywords.join(" ");
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1;
}
return 0;
}}
/>
@joblab have you tried this? How do I pass the keywords to the filter function?
@malun22 Yeah, I use the above code in production. The Command Component automatically passes the keywords for each Command item as the third argument to the filter function you pass to the Command Component via the filter prop.
@joblab hm I see. Cool concept, but my keywords list seems to be empty always even when I give the Item the keyword property.
I have a workaround
filter = {(value, search) => { const label = newOptions.find((item) => item.value === value).label.toLowerCase() if (label.includes(search.toLowerCase())) return 1 return 0 }}
I've been developing a reusable FormCombobox component and I think I can offer a solution to the issue you're facing. Here's a brief overview:
Solution: The approach involves mapping the value received from the filter attribute to the corresponding item and then performing a search based on the item's label. Here's the implementation:
<Command
filter={(value, search) => {
const item = items.find(item => item.value === value)
if (!item) return 0
if (item.label.toLowerCase().includes(search.toLowerCase()))
return 1
return 0
}}
>
Here is a short preview of the component in action:
The full component code is as follows:
'use client'
import React from 'react'
import { FieldValues, type Path, useFormContext } from 'react-hook-form'
import { Check, ChevronsUpDown } from 'lucide-react'
// Shadcn/ui imports ...
export type LabelValuePair = {
value: string
label: string
}
export type FormComboboxProps<T> = {
path: Path<T>
items: LabelValuePair[]
resourceName: string
label?: string
description?: string
}
export function FormCombobox<T extends FieldValues>({
path,
label,
items,
description,
resourceName,
}: FormComboboxProps<T>) {
const { control, setValue } = useFormContext<T>()
return (
<FormField<T>
control={control}
name={path}
render={({ field }) => (
<FormItem className='flex flex-col'>
<FormLabel>{label}</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
role='combobox'
aria-haspopup='listbox'
className={cn(
'justify-between',
!field.value && 'text-muted-foreground',
)}
>
{field.value && field.value.length > 0
? (() => {
const joinedItems = items
.filter(item => field.value.includes(item.value))
.map(item => item.label)
.join(', ')
return joinedItems.length > 50
? joinedItems.slice(0, 50) + '...'
: joinedItems
})()
: `Select ${resourceName}...`}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-full md:w-[300px] p-0'>
<Command
filter={(value, search) => {
const item = items.find(item => item.value === value)
if (!item) return 0
if (item.label.toLowerCase().includes(search.toLowerCase()))
return 1
return 0
}}
>
<CommandInput placeholder={`Search ${resourceName}...`} />
<CommandEmpty>No {resourceName} found.</CommandEmpty>
<CommandGroup>
<CommandList>
{items.map(({ value, label }) => (
<CommandItem
key={value}
value={value}
onSelect={value => {
const currentValues: string[] = field.value || []
if (currentValues.includes(value)) {
field.onChange(
currentValues.filter(item => item !== value),
)
} else {
field.onChange([...currentValues, value])
}
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
field.value && field.value.includes(value)
? 'opacity-100'
: 'opacity-0',
)}
/>
{label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)
}
<FormCombobox<CreateCrewFormValues>
label={'Users'}
path={'users'}
items={items}
resourceName={'users'}
description={`Add users to the group`}
/>
Please don't mind the messiness in the code; it's still a bit of a work in progress. Feel free to use or modify this snippet as needed for your project :))
I combined the ids and name in values and I made a search with name this is working well for me. and while setting the value I split them to get my value
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value
? data.find((framework: any) => framework.carrier_id === value)
?.name
: "Select framework..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{data.map((framework: any) => (
<CommandItem
key={framework.carrier_id}
value={`${framework.carrier_id},${framework.name}`}
onSelect={(currentValue) => {
setValue(
currentValue === value ? "" : currentValue.split(",")[0]
);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === framework.value ? "opacity-100" : "opacity-0"
)}
/>
{framework.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
This is my demo data if it helps:
const testData = [
{
name: "Carrier 1",
carrier_id: "ID_1",
},
{
name: "Carrier 2",
carrier_id: "ID_2",
},
{
name: "Carrier 3",
carrier_id: "ID_3",
}
];
Hi guys, I tried all the above solutions but none worked for me. This is what worked for me. I liked it because I can use any keywords I want without depending on label
const encodeValue = (value: string, keywords: string[] = []) =>
`${keywords.join("|")}|${value}`;
const decodeValue = (value: string) => value.split("|").at(-1) as string;
const items = [{label: "Salary", value: "1", keywords: ["salary", "income"]}]
<Command className="w-full">
<CommandInput placeholder="Search item..." />
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup>
{items.map((item) => {
return (
<>
<CommandItem
key={item.value}
value={encodeValue(item.value, item.keywords)} // encode item with keywords to be able to search with keywords
onSelect={(currentValue) => {
console.log(decodeValue(currentValue)); // get item actual here
}}
className="flex items-center justify-between"
>
<div className="flex items-center justify-center">
<Check
className={cn(
"mr-2 h-4 w-4",
value === item.value ? "opacity-100" : "opacity-0"
)}
/>
{item.label}
</div>
</CommandItem>
</>
);
})}
</CommandGroup>
</Command>
Hey @salman486 just to give you some more things to look into. cmdk itself offers a keywords property on the CommandItem
/** Optional keywords to match against when filtering. */
keywords?: string[]
Which it will use for the search. Then you can just use the value to be unique without the hacks you added. The fuzzy match logic can be a bit too fuzzy but for that you can override the filter property on the Command component.
filter?: (value: string, search: string, keywords?: string[]) => number
Hey @salman486 just to give you some more things to look into. cmdk itself offers a keywords property on the CommandItem
/** Optional keywords to match against when filtering. */ keywords?: string[]
Which it will use for the search. Then you can just use the value to be unique without the hacks you added. The fuzzy match logic can be a bit too fuzzy but for that you can override the filter property on the Command component.
filter?: (value: string, search: string, keywords?: string[]) => number
I have tried using that the issue is that in filter function in Command component it always gives me keywords = []. I am using cmdk version ^0.2.1
I just used it in a project and it was working fine so I would assume it was added with 1.0.0
I've implemented a workaround using the Command component's filter feature. Here's how I've set it up:
<Popover
open={comboboxIsOpen}
onOpenChange={setComboboxIsOpen}
>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={comboboxIsOpen}
className='w-[200px] justify-between'
>
{comboboxValue
? platforms.find(
(platform) =>
platform.value === comboboxValue
)?.label
: 'Selecione uma Plataforma'}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-[200px] p-0'>
<Command
filter={(value, search) => {
const sanitizedSearch = search.replace(
/[-\/\\^$*+?.()|[\]{}]/g,
'\\$&'
);
const searchRegex = new RegExp(
sanitizedSearch,
'i'
);
const platformLabel =
platforms.find(
(platform) => platform.value === value
)?.label || '';
return searchRegex.test(platformLabel) ? 1 : 0;
}}
>
<CommandInput placeholder='Buscar Plataforma...' />
<CommandList>
<CommandEmpty>
Nenhuma plataforma encontrada.
</CommandEmpty>
<CommandGroup>
{platforms.map((platform) => (
<CommandItem
key={platform.value}
value={platform.value}
onSelect={(currentValue) => {
setComboboxValue(
currentValue === comboboxValue
? ''
: currentValue
);
setComboboxIsOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
comboboxValue === platform.value
? 'opacity-100'
: 'opacity-0'
)}
/>
{platform.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
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.
Can this be re-opened?
Of course.
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.