ui icon indicating copy to clipboard operation
ui copied to clipboard

ComboBox search behavior

Open typeleven opened this issue 1 year ago • 24 comments

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.

chrome_2023-05-25_22-57-53

typeleven avatar May 26 '23 05:05 typeleven

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.

olsio avatar May 26 '23 12:05 olsio

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.

Screenshot 2023-05-27 at 09 58 58

olsio avatar May 27 '23 08:05 olsio

I was also struggling with the same issue, thanks for the fix @olsio

iZaL avatar May 27 '23 10:05 iZaL

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

miquelvir avatar Oct 21 '23 20:10 miquelvir

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?

vatoer avatar Dec 13 '23 16:12 vatoer

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>

toyamarodrigo avatar Jan 01 '24 03:01 toyamarodrigo

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.

Aqib-Rime avatar Jan 12 '24 12:01 Aqib-Rime

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?

nimeshmaharjan1 avatar Feb 14 '24 11:02 nimeshmaharjan1

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.

MartinMorici avatar Mar 08 '24 19:03 MartinMorici

Combobox is the weakest component I've used for these reasons

collinversluis avatar Mar 08 '24 22:03 collinversluis

This needs an update asap.

Jupkobe avatar Mar 18 '24 19:03 Jupkobe

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

Screenshot 2024-03-20 at 14 51 26

shomyx avatar Mar 20 '24 14:03 shomyx

Thank you @shomyx you both helped me fix the cmdk update to v1 and the search :)

adriangalilea avatar Mar 26 '24 11:03 adriangalilea

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 avatar Mar 31 '24 21:03 joblab

@joblab have you tried this? How do I pass the keywords to the filter function?

malun22 avatar Apr 03 '24 14:04 malun22

@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 avatar Apr 03 '24 14:04 joblab

@joblab hm I see. Cool concept, but my keywords list seems to be empty always even when I give the Item the keyword property.

malun22 avatar Apr 03 '24 15:04 malun22

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 }}

sachinit254 avatar Apr 10 '24 03:04 sachinit254

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: CleanShot 2024-04-18 at 23 40 28

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 :))

JadRizk avatar Apr 18 '24 21:04 JadRizk

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",
  }
];

saad17shaikh avatar May 09 '24 06:05 saad17shaikh

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>

salman486 avatar May 09 '24 11:05 salman486

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

olsio avatar May 09 '24 12:05 olsio

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

salman486 avatar May 09 '24 14:05 salman486

I just used it in a project and it was working fine so I would assume it was added with 1.0.0

olsio avatar May 09 '24 14:05 olsio

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>

TiberioBrasil avatar May 21 '24 03:05 TiberioBrasil

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.

shadcn avatar Jun 30 '24 23:06 shadcn

Can this be re-opened?

BrendanC23 avatar Jul 11 '24 18:07 BrendanC23

Of course.

shadcn avatar Jul 11 '24 18:07 shadcn

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.

shadcn avatar Aug 05 '24 23:08 shadcn