pacocoursey / cmdk

Fast, unstyled command menu React component.
https://cmdk.paco.me
MIT License
9.04k stars 259 forks source link

Optional custom search key #181

Closed amirakbulut closed 4 months ago

amirakbulut commented 9 months ago

First of all, thank you for all the great work.

In a lot of cases you query your API by id instead of name, because of that we set the value property of selectable items to be equal to their id.

The component however, compares search queries against this value property, which is totally fine and expected, but it requires us to search our items byid instead of readable names. Which is not that user friendly.

Is there a way to search by a specific property, but have the value property still be equal to id?

Maybe something like value="id" searchValue="name"

AntonioErdeljac commented 9 months ago

I ran into this problem, while I have not found the props to do exactly what you describe, I used "value" a a combination of my label and unique ID. (This is Shadcn UI's Command component, but it uses cmdk inside)

<CommandDialog open={isOpen} onOpenChange={onClose}>
    <CommandInput placeholder="Type a command or search..." />
    <CommandList>
      <CommandEmpty>No results found.</CommandEmpty>
      <CommandGroup heading="Documents">
        {documents?.map((document) => (
          <CommandItem
            value={`${document._id}-${document.title}`}
            title={document.title}
            onSelect={() => onSelect(document._id)} 
            key={document._id}
          >
            <FileText className="mr-2 h-4 w-4" />
            <span>{document.title}</span>
          </CommandItem>
        ))}
      </CommandGroup>
    </CommandList>
  </CommandDialog>

This is definitely a step into better UX, but you can also search by "id" so users might get confused why they are seeing certain results if they type uuid for example haha

EduartePaiva commented 8 months ago

I'm with the same issue, I wanted to be able to search by the name, but I wanted to be able to assign the ID of the item when I the use select the item, but doing that searching is disabled. I was doing something like this, it's in chadcn ui but the underline code is cmdk, the selecting part is working but the search part is broken


    <Command>
        <CommandInput placeholder="Procurar exercício..." />
        <CommandEmpty>Nenhum exercício encontrado.</CommandEmpty>
        <CommandGroup>
            {exercicios.map((exercicio) => (
                <CommandItem
                    key={exercicio.id}
                    onSelect={(currentValue) => {
                        setValue(currentValue === value ? "" : currentValue)
                        setOpen(false)
                    }}
                    value={exercicio.id}
                    title={exercicio.nome}
                >
                    <Check
                        className={cn(
                            "mr-2 h-4 w-4",
                            value === exercicio.id ? "opacity-100" : "opacity-0"
                        )}
                    />
                    {exercicio.nome}
                </CommandItem>
            ))}
        </CommandGroup>
    </Command>
fsa317 commented 8 months ago

I would love to do this as well!

taro-ishihara commented 8 months ago

I think you can disable filter totally and make state based custom filter by yourself. <Command shouldFilter={false}> This is so much easier

guizmo commented 7 months ago

here is my solution, using Shadcn/ui

Key points"

"use client";
import React, { use, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
} from "@/components/ui/command";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";

export interface DropdownListItem {
  id: number;
  name: string;
}

export interface DropdownProps {
  list: DropdownListItem[];
}

export default function DropdownCommand({ list }: DropdownProps) {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState("");
  const [search, setSearch] = useState("");

  useEffect(() => {
    if (open) {
      setSearch("");
    }
  }, [open]);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="secondary"
          role="combobox"
          aria-expanded={open}
          className="w-[350px] justify-between"
        >
          {value
            ? list.find((item) => `${item.id}` === value)?.name
            : "Choisir une catégorie"}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[200px] p-0">
        <Command>
          <CommandInput
            placeholder="Search framework..."
            className="h-9"
            value={search}
            onValueChange={setSearch}
          />
          <CommandEmpty>No framework found.</CommandEmpty>
          <CommandGroup>
            {list.map((item) => (
              <CommandItem
                key={`${item.id}`}
                value={`${item.name}`}
                title={item.name}
                onSelect={() => {
                  const currentValue = list.find((el) => el.id === item.id);
                  setValue(
                    `${currentValue?.id}` === value
                      ? ""
                      : `${currentValue?.id}`,
                  );
                  setOpen(false);
                }}
              >
                {item.name}
                <Check
                  className={cn(
                    "ml-auto h-4 w-4",
                    value === `${item.id}` ? "opacity-100" : "opacity-0",
                  )}
                />
              </CommandItem>
            ))}
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>
  );
}
nstylo commented 7 months ago

+1, also currently bumping into the same issue. I think there should be an option to define the search key.

csalmeida commented 7 months ago

Great job with this component works really well! 🎉

Initially I thought a search key could be useful but in most cases the filter prop in the Command component could solve the use case of having the ID as a value and wanting to look up by some other key. e.g. name.

In my use case I have an array of teams being passed to the component and command item makes use of the team.id value which is then passed to the form onSelect. This works just fine.

However, the CommandInput seems to default search to team.id as well which makes sense since that's the value selected but the user is interested in look up team names by team.name.

We could argue that having a custom search key would help in this case, or something like searchValue:

 <CommandItem
      key={item.id}
      value={item.id}
      searchValue={item.name} // 👈 Instead of using the id for search we are using the name instead.
      onSelect={(currentValue) => {
          const newValue =
              currentValue === value
                  ? ''
                  : currentValue

          setValue(newValue)
          setOpen(false)

          handleChange(newValue)
      }}
  >

Even if this works (I am not sure how internally the filtering occurs) it feels like we can't control a lot of how the filtering works. Using the filter prop I was able to keep the ID as a value to use it in the form and use the name for the search:

    <Command
        // Custom filter so that search looks up by name instead of id 
        filter={(value, search) => {
            const teamName =
                teams
                    ?.find((item) => item.id === value)
                    ?.name?.toLowerCase() || ''
            if (teamName.includes(search)) return 1
            return 0
        }}
    >
varna commented 6 months ago

So, I just pushed all strings that I need to be searchable to the value, and set the id manually onSelect:

<CommandItem
  key={item.id}
  value={`${item.name} - ${item.nativeName}`}
  onSelect={() => {
    onChange(item.id)
    setOpen(false)
  }}
/>
pacocoursey commented 5 months ago

Yeah, I think using value was a mistake. I'll consider how to fix this for the next release. In the meantime, you should be able to use keywords to add additional matching strings, in case that's helpful.

McTom234 commented 5 months ago

@pacocoursey I found a (in my opinion - works well for me) solution using an id-value map. I made a fork for work and could prepare a small PR within the next hours if you think that an id-value map would be cool, at least as a work-around.

PS: I just saw that you added keywords only today - that's not a big difference to what I did using a map. PSS: Although, adding the ID to the keywords might have performance impact on the search/filter algorithm? and could cause unwanted results (e.g., when using not numeric IDs).

Therefore, I would suggest adding an optional ID value to the item component or using a map provided on the root component where users can assign the ID to the value and the filter will get the text value of that item, but the value prop of the root component would be the ID of the item(s).

I think, the ID prop should be the preferred solution, because the map would rely on the value prop of the item to be the ID (which works in my use cases, but may not for everyone).

I would be willing to work on that and open a PR this week.

jtapeg commented 5 months ago

I noticed that version 0.2.1 still doesn't have keywords

snoot-booper commented 5 months ago

Same, I'm trying to use keywords to circumvent the search key problem like suggested here and I see the PR is merged. But 0.2.1 does not include it @pacocoursey

kevin-dapps commented 4 months ago

+1

afrieirham commented 4 months ago

If anyone looking for a safe workaround, here's mine to consider.

Problem:

Solution:

// replaceAll is needed because it won't work with double quotes


```tsx
// for the filter function
<Command
  filter={(value, search) => {
    const { name } = JSON.parse(value.replaceAll("'", '"')) as { id: string; name: string; };
    if (name?.includes(search)) return 1;
    return 0;
  }}
></Command>;

Note that this is just a temporary workaround. As soon as the keywords prop available, I'll switch to it.

You can refer my full implementation here

divmgl commented 4 months ago

I just now ran into a similar situation too and my mind immediately went to "how do I set the search key?" But after working with this for a bit I think I prefer how cmdk does this: just set a custom filter. This lets you, the implementer, decide how you want it to work when the defaults don't do what you want.

Here's a contrived example.

import { Command, CommandGroup, CommandItem } from "./ui/command"

type Car = {
  id: string
  name: string
}

type CarsDropdownProps = {
  cars: Car[]
}

function CarsDropdown({ cars }: CarsDropdownProps) {
  // Create a map to avoid O(n) lookups
  const carsMap = cars.reduce(
    (acc, car) => {
      acc[car.id] = car // Use the unique identifier as the key
      return acc
    },
    {} as Record<string, Car>
  )

  return (
    <Command
      filter={(value, search) => {
        const car = carsMap[value]
        // Basic search but you can extend this
        if (car && car.name.toLowerCase().includes(search.toLowerCase())) return 1
        return 0
      }}
    >
      <CommandGroup>
        {cars.map((car) => (
          <CommandItem key={car.id} value={car.id}>
            {car.name}
          </CommandItem>
        ))}
      </CommandGroup>
    </Command>
  )
}
asontha commented 4 months ago

Any update on this?

pacocoursey commented 4 months ago

keywords prop is available in v1.0.0.

https://github.com/pacocoursey/cmdk/releases/tag/v1.0.0