Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@ export default function Home() {
`You have selected following frameworks: ${data.frameworks.join(", ")}.`
);
}

const handleCreateFramework = (newFramework: string) => {
const newFrameworkObject = {
value: newFramework.toLowerCase(),
label: newFramework,
icon: Fish, // Using Fish icon as default for new frameworks
};
frameworksList.push(newFrameworkObject);
};
return (
<main className="flex min-h-screen:calc(100vh - 3rem) flex-col items-center justify-start space-y-3 p-3">
<PageHeader>
Expand Down Expand Up @@ -109,6 +116,8 @@ export default function Home() {
defaultValue={field.value}
placeholder="Select options"
variant="inverted"
creatable={true}
onCreate={handleCreateFramework}
animation={2}
maxCount={3}
/>
Expand Down
39 changes: 38 additions & 1 deletion src/components/multi-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ChevronDown,
XIcon,
WandSparkles,
Plus
} from "lucide-react";

import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -115,6 +116,18 @@ interface MultiSelectProps
* Optional, can be used to add custom styles.
*/
className?: string;

/**
* If true, allows creating new options that are not in the original list.
* Optional, defaults to false.
*/
creatable?: boolean;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need this creatable prop - we could make it creatable if an onCreate function is provided. Otherwise, the two props depend on each other, and it's possible to submit invalid values by e.g. setting creatable={true} and not providing onCreate, or providing onCreate without making it creatable


/**
* Callback function triggered when a new option is created.
* Required if creatable is true.
*/
onCreate?: (value: string) => void;
}

export const MultiSelect = React.forwardRef<
Expand All @@ -133,6 +146,8 @@ export const MultiSelect = React.forwardRef<
modalPopover = false,
asChild = false,
className,
creatable = false,
onCreate,
...props
},
ref
Expand All @@ -141,6 +156,7 @@ export const MultiSelect = React.forwardRef<
React.useState<string[]>(defaultValue);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");

React.useEffect(() => {
if (JSON.stringify(selectedValues) !== JSON.stringify(defaultValue)) {
Expand Down Expand Up @@ -194,6 +210,18 @@ export const MultiSelect = React.forwardRef<
}
};

const handleInputChange = (value: string) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't being used, and can be removed

setInputValue(value);
};

const handleCreateOption = () => {
if (creatable && onCreate && inputValue) {
onCreate(inputValue);
toggleOption(inputValue);
setInputValue("");
}
};

return (
<Popover
open={isPopoverOpen}
Expand Down Expand Up @@ -295,7 +323,16 @@ export const MultiSelect = React.forwardRef<
onKeyDown={handleInputKeyDown}
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CommandInput also has its own copy of the search input at the moment: https://github.com/pacocoursey/cmdk/blob/main/cmdk/src/index.tsx#L786

I think it would be better to use the controlled version of the input by passing in inputValue and setInputValue, as described here: https://github.com/pacocoursey/cmdk/tree/main?tab=readme-ov-file#input-cmdk-input

It would also be a good idea to rename them to search and setSearch, to match the naming from cmdk

<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandEmpty>
{creatable ? (
<CommandItem onSelect={handleCreateOption}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be displayed, because the filter will automatically hide this CommandItem when it doesn't match the search input. I've ended up with the following code:

<CommandEmpty className={cn({ p0: !!onCreate })}>
  {onCreate ? (
    <CommandGroup forceMount>
      <CommandItem
        forceMount
        onSelect={() => {
          if (search) {
            onCreate(search);
            toggleOption(search);
            setSearch("");
          }
        }}
      >
        <Plus className="mr-2 h-4 w-4" />
        Create &quot;{search}&quot;
      </CommandItem>
    </CommandGroup>
  ) : (
    "No results found."
  )}
</CommandEmpty>

The most important parts are wrapping the CommandItem in a CommandGroup, and adding forceMount to both the item and the group

<Plus className="mr-2 h-4 w-4" />
Create "{inputValue}"
</CommandItem>
) : (
"No results found."
)}
</CommandEmpty>
<CommandGroup>
<CommandItem
key="all"
Expand Down