Multi Select
'use client';
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { CheckIcon, PlusIcon } from 'lucide-react';
import * as z from 'zod';
import { Button } from '@/components/plate-ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/plate-ui/form';
import {
type SelectItem,
SelectEditor,
SelectEditorCombobox,
SelectEditorContent,
SelectEditorInput,
} from '@/components/plate-ui/select-editor';
const LABELS = [
{ url: '/docs/components/editor', value: 'Editor' },
{ url: '/docs/components/select-editor', value: 'Select Editor' },
{ url: '/docs/components/block-selection', value: 'Block Selection' },
{ url: '/docs/components/button', value: 'Button' },
{ url: '/docs/components/command', value: 'Command' },
{ url: '/docs/components/dialog', value: 'Dialog' },
{ url: '/docs/components/form', value: 'Form' },
{ url: '/docs/components/input', value: 'Input' },
{ url: '/docs/components/label', value: 'Label' },
{ url: '/docs/components/plate-element', value: 'Plate Element' },
{ url: '/docs/components/popover', value: 'Popover' },
{ url: '/docs/components/tag-element', value: 'Tag Element' },
] satisfies (SelectItem & { url: string })[];
const formSchema = z.object({
labels: z
.array(
z.object({
value: z.string(),
})
)
.min(1, 'Select at least one label')
.max(10, 'Select up to 10 labels'),
});
type FormValues = z.infer<typeof formSchema>;
export default function EditorSelectForm() {
const [readOnly, setReadOnly] = React.useState(false);
const form = useForm<FormValues>({
defaultValues: {
labels: [LABELS[0]],
},
resolver: zodResolver(formSchema),
});
const labels = useWatch({ control: form.control, name: 'labels' });
return (
<div className="mx-auto w-full max-w-2xl space-y-8 p-11 pl-2 pt-24">
<Form {...form}>
<div className="space-y-6">
<FormField
name="labels"
control={form.control}
render={({ field }) => (
<FormItem>
<div className="flex items-start gap-2">
<Button
variant="ghost"
className="h-10"
onClick={() => setReadOnly(!readOnly)}
type="button"
>
{readOnly ? (
<PlusIcon className="size-4" />
) : (
<CheckIcon className="size-4" />
)}
</Button>
{readOnly && labels.length === 0 ? (
<Button
size="lg"
variant="ghost"
className="h-10"
onClick={() => {
setReadOnly(false);
}}
type="button"
>
Add labels
</Button>
) : (
<FormControl>
<SelectEditor
value={field.value}
onValueChange={readOnly ? undefined : field.onChange}
items={LABELS}
>
<SelectEditorContent>
<SelectEditorInput
readOnly={readOnly}
placeholder={
readOnly ? 'Empty' : 'Select labels...'
}
/>
{!readOnly && <SelectEditorCombobox />}
</SelectEditorContent>
</SelectEditor>
</FormControl>
)}
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
</Form>
</div>
);
}
Features
Unlike traditional input-based multi-selects, this component is built on top of Plate editor, providing:
- Full history support (undo/redo)
- Native cursor navigation between and within tags
- Select one to many tags
- Copy/paste tags
- Drag and drop to reorder tags
- Read-only mode
- Duplicate tags prevention
- Create new tags, case insensitive
- Search text cleanup
- Whitespace trimming
- Fuzzy search with cmdk
Installation
npm install @udecode/plate-tag
Usage
import { MultiSelectPlugin } from '@udecode/plate-tag/react';
import { TagElement } from '@/components/plate-ui/tag-element';
import {
SelectEditor,
SelectEditorContent,
SelectEditorInput,
SelectEditorCombobox,
type SelectItem,
} from '@/components/plate-ui/select-editor';
// Define your items
const ITEMS: SelectItem[] = [
{ value: 'React' },
{ value: 'TypeScript' },
{ value: 'JavaScript' },
];
export default function MySelectEditor() {
const [value, setValue] = React.useState<SelectItem[]>([ITEMS[0]]);
return (
<SelectEditor
value={value}
onValueChange={setValue}
items={ITEMS}
>
<SelectEditorContent>
<SelectEditorInput placeholder="Select items..." />
<SelectEditorCombobox />
</SelectEditorContent>
</SelectEditor>
);
}
Plugins
TagPlugin
Inline void element plugin.
MultiSelectPlugin
Extension of the TagPlugin that constrains the editor to tag elements.
API
editor.tf.insert.tag
Inserts a new multi-select element at the current selection.
Parameters
Properties for the multi-select element:
Hooks
useSelectedItems
Hook to get the currently selected tag items in the editor.
Returns
Array of selected tag items, each containing a value and any additional properties.
getSelectedItems
Gets all tag items in the editor.
Parameters
The Slate editor instance.
Returns
Array of tag items in the editor.
isEqualTags
Utility function to compare two sets of tags for equality, ignoring order.
Parameters
The Slate editor instance.
New set of tags to compare against the current editor tags.
Returns
true
if both sets contain the same values, false
otherwise.
useSelectableItems
Hook to get the available items that can be selected, filtered by search and excluding already selected items.
Parameters
Returns
Filtered array of selectable items.
useSelectEditorCombobox
Hook to handle combobox behavior in the editor, including text cleanup and item selection.
Parameters
Types
TTagElement
type TTagElement = TElement & {
value: string;
[key: string]: unknown;
};
TagLike
type TagLike = {
value: string;
[key: string]: unknown;
};