Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ComboBox search behavior #458

Closed
typeleven opened this issue May 26, 2023 · 32 comments
Closed

ComboBox search behavior #458

typeleven opened this issue May 26, 2023 · 32 comments
Labels

Comments

@typeleven
Copy link

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
Copy link
Contributor

olsio commented May 26, 2023

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
Copy link
Contributor

olsio commented May 27, 2023

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
Copy link

iZaL commented May 27, 2023

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

@miquelvir
Copy link
Contributor

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
Copy link

vatoer commented Dec 13, 2023

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
Copy link

toyamarodrigo commented Jan 1, 2024

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
Copy link

Aqib-Rime commented Jan 12, 2024

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
Copy link

nimeshmaharjan1 commented Feb 14, 2024

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
Copy link

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
Copy link

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

@Jupkobe
Copy link

Jupkobe commented Mar 18, 2024

This needs an update asap.

@shomyx
Copy link

shomyx commented Mar 20, 2024

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
Copy link

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

@joblab
Copy link

joblab commented Mar 31, 2024

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
Copy link

malun22 commented Apr 3, 2024

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

@joblab
Copy link

joblab commented Apr 3, 2024

@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
Copy link

malun22 commented Apr 3, 2024

@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
Copy link

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
Copy link

JadRizk commented Apr 18, 2024

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
Copy link

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
Copy link

salman486 commented May 9, 2024

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
Copy link
Contributor

olsio commented May 9, 2024

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
Copy link

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
Copy link
Contributor

olsio commented May 9, 2024

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

@TiberioBrasil
Copy link

TiberioBrasil commented May 21, 2024

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 shadcn added the Stale label Jun 22, 2024
@shadcn
Copy link
Collaborator

shadcn commented Jun 30, 2024

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 shadcn closed this as completed Jun 30, 2024
@BrendanC23
Copy link

Can this be re-opened?

@shadcn
Copy link
Collaborator

shadcn commented Jul 11, 2024

Of course.

@shadcn shadcn reopened this Jul 11, 2024
@shadcn shadcn removed the Stale label Jul 12, 2024
@shadcn shadcn added the Stale label Jul 28, 2024
@shadcn
Copy link
Collaborator

shadcn commented Aug 5, 2024

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 shadcn closed this as completed Aug 5, 2024
@esmaeil-gh24
Copy link

If you want to search in labels only (and not in values) use this:

First put label inside value.
item = { value: `${option.value} ${option.lable}`, label: option.label }

Then split value inside filter function. (I'm using substring instead of split[1] in case there was an space in the label)

filter={(value, search) => {
  const label = value.substring(value.indexOf(' ') + 1)
  if (label.includes(search)) return 1
  return 0
}}```

@Jordan-Hall
Copy link

@shadcn can this be reopen

@Lexachoc
Copy link

image

The original issue seems fixed, but it is still a bit confusing that when typing nextt, for example, the item next.js is returned.
However, this is the default filter behaviour of cmdk (under the hood).

For someone like me who wants to do an exact search, you can do the following:

<Command
  filter={(value, search) => {
    if (value.includes(search)) return 1
    return 0
  }}
/>

as stated in the here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests