shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
66.59k stars 3.88k forks source link

ComboBox search behavior #458

Closed typeleven closed 1 week ago

typeleven commented 1 year ago

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

olsio commented 1 year ago

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 commented 1 year ago

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

iZaL commented 1 year ago

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

miquelvir commented 10 months ago

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

vatoer commented 8 months ago

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?

toyamarodrigo commented 7 months ago

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>
Aqib-Rime commented 7 months ago

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.

nimeshmaharjan1 commented 6 months ago

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?

MartinMorici commented 5 months ago

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.

collinversluis commented 5 months ago

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

Jupkobe commented 5 months ago

This needs an update asap.

shomyx commented 5 months ago

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

adriangalilea commented 4 months ago

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

joblab commented 4 months ago

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;
  }}
/>
malun22 commented 4 months ago

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

joblab commented 4 months ago

@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.

malun22 commented 4 months ago

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

sachinit254 commented 4 months ago

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

JadRizk commented 4 months ago

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

saad17shaikh commented 3 months ago

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",
  }
];
salman486 commented 3 months ago

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>
olsio commented 3 months ago

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

salman486 commented 3 months ago

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

olsio commented 3 months ago

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

TiberioBrasil commented 2 months ago

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>
shadcn commented 1 month ago

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.

BrendanC23 commented 1 month ago

Can this be re-opened?

shadcn commented 1 month ago

Of course.

shadcn commented 1 week ago

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.