Search Components...

Multi Select - React Hook Form

A Multi Select component that uses react-hook-form Controller.

Installation

This component is dependent on floating-button component. Please add that component first before adding this one.

1. Copy and paste the following code into your project.

"use client";
import * as React from "react";
import { Controller, type FieldPath, type FieldValues, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { FloatingLabelButon } from "@/components/ui/buttons/floating-label-button";
import { ChevronsUpDown, Square, SquareCheckBig } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 
// ----------------------------------------------------------------------
 
type SelectOption = {
	label: string;
	value: string;
};
 
interface CustomRHFMultiSelectProps<TFieldValues extends FieldValues>
	extends Omit<React.ComponentPropsWithoutRef<typeof Controller>, "name" | "control" | "render"> {
	name: FieldPath<TFieldValues>;
	btnProps?: Omit<React.ComponentPropsWithoutRef<typeof FloatingLabelButon>, "value" | "label">;
	popoverProps?: React.ComponentPropsWithoutRef<typeof PopoverContent>;
	helperText?: string;
	containerClass?: string;
	label: string;
	options: SelectOption[];
}
 
export default function CustomRHFMultiSelect<TFieldValues extends FieldValues>({
	name,
	label,
	helperText,
	defaultValue,
	rules,
	containerClass,
	btnProps = {
		variant: "outline",
		size: "md",
		className: "w-full justify-between",
	},
	popoverProps,
	options,
}: CustomRHFMultiSelectProps<TFieldValues>) {
	const btnRef = React.useRef<React.ElementRef<"button">>(null);
	const [open, setOpen] = React.useState(false);
	const { control } = useFormContext();
 
	const handleValueChange = React.useCallback((value: string, inputValue: string, onChange: (...event: any[]) => void) => {
		const values = inputValue.trim() === "" ? [] : inputValue.split(",");
		const valIdx = values.findIndex((val) => val === value);
		if (valIdx === -1) {
			values.push(value);
		} else {
			values.splice(valIdx, 1);
		}
		onChange(values.join(","));
		setOpen(false);
	}, []);
 
	const popOverStyle = !!btnRef.current ? { width: btnRef.current.clientWidth } : {};
	return (
		<Controller
			name={name}
			control={control}
			defaultValue={defaultValue}
			rules={rules}
			render={({ field, fieldState: { error } }) => {
				return (
					<div className={containerClass}>
						<Popover>
							<PopoverTrigger asChild>
								<FloatingLabelButon
									role={name + "-popover"}
									aria-expanded={open}
									{...btnProps}
									ref={btnRef}
									label={label}
									value={field.value}
									endIcon={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
									error={!!error}
								/>
							</PopoverTrigger>
							<PopoverContent
								{...popoverProps}
								style={popOverStyle}
								className={cn("p-0 space-y-0 w-full min-w-[--radix-popper-anchor-width]", popoverProps?.className)}>
								{options.map((option, index) => {
									const selected = (field.value as string).split(",").findIndex((val) => val === option.value) !== -1;
									return (
										<div
											key={index}
											onClick={() => handleValueChange(option.value, field.value, field.onChange)}
											className={cn(
												"flex items-center rounded-md py-2 px-1.5 cursor-pointer select-none hover:bg-accent dark:hover:bg-accent/10",
												{
													"font-medium bg-accent dark:bg-accent/10": selected,
												},
											)}>
											{selected ? <SquareCheckBig className="h-4 w-4" /> : <Square className="h-4 w-4" />}
											<span className="text-sm ml-2 whitespace-nowrap">{option.label}</span>
										</div>
									);
								})}
							</PopoverContent>
						</Popover>
						<div className="space-y-2 mt-2">
							{!!helperText && <p className="text-sm text-muted-foreground ml-1.5">{helperText}</p>}
							{!!error && <p className="text-sm text-error ml-1.5">{error?.message}</p>}
						</div>
					</div>
				);
			}}
		/>
	);
}

2. Add this to your @/components/ui/hook-form/index.tsx file for easy import.

export { default as RHFMultiSelect } from "./rhf-multi-select";

Shadcn

This multi-select component is built on top of the excellent foundation provided by Shadcn/UI popover component.

For a deeper dive into the core concepts and building blocks, check out the original Shadcn popover component.