[bug]: Combo box is not working.
Describe the bug
the default code for combobox in a next.js app throws this error:
Unhandled Runtime Error TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
Affected component/components
ComboBox
How to reproduce
just copy the default code for combo box from shadcnui.com/combobox and paste it in page.tsx run the code and the error should pop up while trying to click the PopoverTrigger button.
Codesandbox/StackBlitz link
No response
Logs
No response
System Info
browser
Before submitting
- [ ] I've made research efforts and searched the documentation
- [x] I've searched for existing issues
me too. I use the example code in the popover anchor, maybe we should add the CommandList comp:
maybe the cmdk package has some changes and the shadcnui not response to.
you need to use this version, v1 doesnt work
"cmdk": "^0.2.1",
or
tailwind.config.ts
plugins: [
require("tailwindcss-animate"),
function ({ addUtilities }) {
addUtilities({
".popover-content-width-same-as-its-trigger": {
width: "var(--radix-popover-trigger-width)",
"max-height": "var(--radix-popover-content-available-height)",
},
});
},
],
my-combobox.tsx
type MyComboboxProps = {
label?: string;
value: string;
setValue: (value: string) => void;
values: { label: string; value: string }[];
inputDisabled?: boolean;
inputPlaceholder?: string;
};
export const MyCombobox: React.FC<MyComboboxProps> = (props) => {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<div className="flex flex-col w-full">
{props.label && <Label className="mb-1">{props.label}</Label>}
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className=" justify-between"
>
{props.value
? props.values.find((item) => item.value === props.value)?.label
: props.label + "..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
</div>
<PopoverContent
className={"popover-content-width-same-as-its-trigger p-0"}
>
<Command>
{props.inputDisabled && (
<CommandInput placeholder={props.inputPlaceholder} />
)}
<CommandEmpty>NOTHING_FOUND</CommandEmpty>
<CommandGroup>
<CommandList>
{props.values.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(currentValue) => {
props.setValue(
currentValue === props.value ? "" : currentValue
);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
props.value === item.value ? "opacity-100" : "opacity-0"
)}
/>
{item.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
};
you need to use this version, v1 doesnt work
"cmdk": "^0.2.1",
Great catch, once downgrading I no longer run into the issue.
Just wrap CommandGroup in a CommandList and it will work on latest cmdk. Move the CommandEmpty inside as well.
It should look like this: (just command, popover stuff removed for brevity)
<Command>
<CommandInput placeholder="Search ..." />
<CommandList>
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
return (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue);
setOpen(false);
onValueChange(currentValue);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
{option.label}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
you need to use this version, v1 doesnt work
"cmdk": "^0.2.1",
Weekend saved, nice catch
Yo bro, the command box may not be working for 2 reasons.
- You didnt wrap the CommantGroup or CommandItem with the CommandList, its required with the current relaease as of this date.
- The exact issue i had struggled with for over 6hours. Go to your Command component, and paste the code below to overwrite the default. Or simply replace every className with data-[disabled] to data-[disabled=true].
"use client";
import * as React from "react"; import { type DialogProps } from "@radix-ui/react-dialog"; import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef< React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>
(({ className, ...props }, ref) => ( <CommandPrimitive ref={ref} className={cn( "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className )} {...props} /> )); Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( <Dialog {...props}> <DialogContent className="overflow-hidden p-0"> <Command className="[&[cmdk-group-heading]]:px-2 [&[cmdk-group-heading]]:font-medium [&[cmdk-group-heading]]:text-muted-foreground [&[cmdk-group]:not([hidden])~[cmdk-group]]:pt-0 [&[cmdk-group]]:px-2 [&[cmdk-input-wrapper]svg]:h-5 [&[cmdk-input-wrapper]svg]:w-5 [&[cmdk-input]]:h-12 [&[cmdk-item]]:px-2 [&[cmdk-item]]:py-3 [&[cmdk-item]svg]:h-5 [&[cmdk-item]_svg]:w-5"> {children} </Command> </DialogContent> </Dialog> ); };
const CommandInput = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
(({ className, ...props }, ref) => (
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef< React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
(({ className, ...props }, ref) => ( <CommandPrimitive.List ref={ref} className={cn( "max-h-[300px] overflow-y-auto overflow-x-hidden", className )} {...props} /> ));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
((props, ref) => ( <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} /> ));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
(({ className, ...props }, ref) => ( <CommandPrimitive.Group ref={ref} className={cn( "overflow-hidden p-1 text-foreground [&[cmdk-group-heading]]:px-2 [&[cmdk-group-heading]]:py-1.5 [&[cmdk-group-heading]]:text-xs [&[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", className )} {...props} /> ));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Separator>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
(({ className, ...props }, ref) => ( <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} /> )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef< React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
(({ className, ...props }, ref) => ( <CommandPrimitive.Item ref={ref} className={cn( "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50", className )} {...props} /> ));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { return ( <span className={cn( "ml-auto text-xs tracking-widest text-muted-foreground", className )} {...props} /> ); }; CommandShortcut.displayName = "CommandShortcut";
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, };
Just wrap
CommandGroupin aCommandListand it will work on latestcmdk. Move theCommandEmptyinside as well.It should look like this: (just command, popover stuff removed for brevity)
<Command> <CommandInput placeholder="Search ..." /> <CommandList> <CommandEmpty>No option found.</CommandEmpty> <CommandGroup> {options.map((option) => { return ( <CommandItem key={option.value} value={option.value} onSelect={(currentValue) => { setValue(currentValue === value ? "" : currentValue); setOpen(false); onValueChange(currentValue); }} > <CheckIcon className={cn( "mr-2 h-4 w-4", value === option.value ? "opacity-100" : "opacity-0", )} /> {option.label} </CommandItem> ); })} </CommandGroup> </CommandList> </Command>
Thanks man.
For me, the issue still exists. No solution here worked, neither upgrading cmdk nor wrapping CommandGroup in a CommandList. Has anyone another impuls? What about this PR? 🤔
For me, the issue still exists. No solution here worked, neither upgrading
cmdknor wrappingCommandGroupin aCommandList. Has anyone another impuls? What about this PR? 🤔
I HAVE FIXED ALL THESE ISSUES... SIMPLY COPY AND PASTE THE BELOW ADVANCED COMBOBOX👇
`"use client"; import { useState, useEffect, useRef } from "react"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { cn } from "@/utils/base_utils"; import { Button } from "./button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; import { Drawer, DrawerContent, DrawerTrigger } from "./drawer"; import { useMediaScreen } from "src/hooks/useMediaScreen"; import { ScrollArea } from "./scroll-area";
// Define a function to convert a string or number to an object with label and value function normalizeItem(item) { if (typeof item === "string" || typeof item === "number") { return { label: item.toString(), value: item.toString().toLowerCase() }; } return { label: item.label, value: item.value.toLowerCase() }; }
export function ComboBox({ array: originalArray, //NOTE: The array of items to display in the dropdown defaultValue, //NOTE: This will be used if provided as the initial placeholder, //NOTE: The placeholder text allowSearch = false, //NOTE: Whether to include a search input in the dropdown onValueChange, //NOTE: A callback function that is called when a new value is selected side, triggerClassName, popoverContentClassName, drawerContentClassName, commandItemClassName, iconLeft, iconRight, }) { const array = originalArray.map(normalizeItem); // Convert the array items to objects const isMobile = useMediaScreen({ breakpoint: "md" }); const [open, setOpen] = useState(false); //NOTE: Whether the dropdown is open const popoverTriggerRef = useRef(null); const [currentValue, setCurrentValue] = useState(""); //NOTE: The currently selected value const prevValueRef = useRef(); const defaultValueRef = useRef(defaultValue);
// NOTE: Pass the curent value to be accessible by parent component for use. useEffect(() => { prevValueRef.current = currentValue; });
const prevValue = prevValueRef.current;
useEffect(() => { if (prevValue !== currentValue && onValueChange) { onValueChange(currentValue ? currentValue : ""); } }, [currentValue, onValueChange, prevValue]);
useEffect(() => { defaultValueRef.current = defaultValue; }, [defaultValue]);
useEffect(() => { if (defaultValueRef.current) { // NOTE: If there is a default value we must cross check to make sure the default value is amongst the array as a value, else we throw error const valueExists = array.find( (item) => item.value === defaultValueRef.current );
if (!valueExists) {
throw new Error(
`The default value "${
defaultValueRef.current
}" does not exist in the provided array. The array contains the following values: ${array
.map((item) => item.value)
.join(", ")}`
);
} else {
setCurrentValue(defaultValueRef.current);
}
}
}, [array]);
//NOTE: Render the mobile version of the dropdown if the screen width is small if (isMobile) { return ( <Drawer open={open} onOpenChange={setOpen}> <DrawerTrigger asChild> <Button variant="outline" onClick={(e) => e.stopPropagation()} className={cn("w-full justify-between gap-2", triggerClassName)} > {iconLeft && iconLeft}
{currentValue ? (
array.find((item) => item.value === currentValue)?.label
) : (
<span className="text-muted-foreground">
{placeholder && placeholder}
</span>
)}
<span className="w-4 h-4 shrink-0 [&_svg]:w-full [&_svg]:h-full">
{iconRight ? iconRight : <CaretSortIcon />}
</span>
</Button>
</DrawerTrigger>
<DrawerContent className={drawerContentClassName}>
<div id="wrapper" className="mt-4 border-t">
<SelectList
currentValue={currentValue}
array={array}
placeholder={placeholder}
allowSearch={allowSearch}
setOpen={setOpen}
setSelected={setCurrentValue}
commandItemClassName={commandItemClassName}
/>
</div>
</DrawerContent>
</Drawer>
);
}
//NOTE: Render the desktop version of the dropdown return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button ref={popoverTriggerRef} variant="outline" role="combobox" onClick={(e) => e.stopPropagation()} aria-expanded={open} className={cn("w-full justify-between gap-2", triggerClassName)} > {iconLeft && iconLeft}
{currentValue ? (
array.find((item) => item.value === currentValue)?.label
) : (
<span className="text-muted-foreground">
{placeholder && placeholder}
</span>
)}
<span className="w-4 h-4 shrink-0 [&_svg]:w-full [&_svg]:h-full">
{iconRight ? iconRight : <CaretSortIcon />}
</span>
</Button>
</PopoverTrigger>
<PopoverContent
side={side}
align="start"
style={{
width: popoverTriggerRef.current
? `${popoverTriggerRef.current.offsetWidth}px`
: "auto",
}}
className={cn("p-0", popoverContentClassName)}
>
<ScrollArea>
<SelectList
currentValue={currentValue}
array={array}
placeholder={placeholder}
allowSearch={allowSearch}
setOpen={setOpen}
setSelected={setCurrentValue}
commandItemClassName={commandItemClassName}
/>
</ScrollArea>
</PopoverContent>
</Popover>
); }
//NOTE: The SelectList component, used to render the list of items in the dropdown const SelectList = ({ currentValue, //NOTE: The currently selected value setOpen, //NOTE: A function to open or close the dropdown setSelected, //NOTE: A function to set the selected value array, //NOTE: The array of items to display in the dropdown allowSearch, //NOTE: Whether to include a search input in the dropdown placeholder, //NOTE: The placeholder text commandItemClassName, renderItem, //NOTE: A function to render each item in the dropdown as jsx }) => { return ( <Command className="w-full"> {allowSearch && ( <> <CommandInput placeholder={placeholder} className="h-9" /> </> )}
<CommandList>
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup>
{array.map((item) => (
<CommandItem
key={item.value}
value={item.value}
onSelect={(value) => {
setSelected(currentValue === value ? "" : value);
setOpen(false);
}}
className={cn("py-2 cursor-pointer", commandItemClassName)}
>
{/*NOTE: Use renderItem if it's provided, else use label if it's available, else use value */}
{renderItem ? renderItem(item) : item.label || item.value}
<CheckIcon
className={cn(
"ml-auto size-5",
currentValue === item.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
); }; `
you need to use this version, v1 doesnt work
"cmdk": "^0.2.1",or
tailwind.config.tsplugins: [ require("tailwindcss-animate"), function ({ addUtilities }) { addUtilities({ ".popover-content-width-same-as-its-trigger": { width: "var(--radix-popover-trigger-width)", "max-height": "var(--radix-popover-content-available-height)", }, }); }, ],
my-combobox.tsxtype MyComboboxProps = { label?: string; value: string; setValue: (value: string) => void; values: { label: string; value: string }[]; inputDisabled?: boolean; inputPlaceholder?: string; }; export const MyCombobox: React.FC<MyComboboxProps> = (props) => { const [open, setOpen] = useState(false); return ( <Popover open={open} onOpenChange={setOpen}> <div className="flex flex-col w-full"> {props.label && <Label className="mb-1">{props.label}</Label>} <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className=" justify-between" > {props.value ? props.values.find((item) => item.value === props.value)?.label : props.label + "..."} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> </div> <PopoverContent className={"popover-content-width-same-as-its-trigger p-0"} > <Command> {props.inputDisabled && ( <CommandInput placeholder={props.inputPlaceholder} /> )} <CommandEmpty>NOTHING_FOUND</CommandEmpty> <CommandGroup> <CommandList> {props.values.map((item) => ( <CommandItem key={item.value} value={item.value} onSelect={(currentValue) => { props.setValue( currentValue === props.value ? "" : currentValue ); setOpen(false); }} > <Check className={cn( "mr-2 h-4 w-4", props.value === item.value ? "opacity-100" : "opacity-0" )} /> {item.label} </CommandItem> ))} </CommandList> </CommandGroup> </Command> </PopoverContent> </Popover> ); };
Nice one. Downgraded the cmdk and it worked. Thank you
Downgrading to "cmdk": "^0.2.1"
Worked 👍 Thanks ❤️
