Multi Select

Loading...
Files
components/select-editor-demo.tsx
'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>
  );
}

See also:

Plugins

TagPlugin

Inline void element plugin.

MultiSelectPlugin

Extension of TagPlugin that constrains editor to tag elements.

API

editor.tf.insert.tag

Inserts new multi-select element at current selection.

Parameters

Collapse all

    Properties for multi-select element.

OptionsTagLike

Collapse all

    Unique value of multi-select element.

Hooks

useSelectedItems

Hook to get currently selected tag items.

ReturnsTagLike[]

    Array of selected tag items with values and properties.

getSelectedItems

Gets all tag items in editor.

Parameters

Collapse all

    The editor instance.

ReturnsTagLike[]

    Array of tag items in editor.

isEqualTags

Compare two sets of tags for equality, ignoring order.

Parameters

Collapse all

    The editor instance.

    New tags to compare against current editor tags.

Returnsboolean

    Whether both sets contain same values.

useSelectableItems

Hook for available items that can be selected.

Parameters

Collapse all

    Options for selectable items.

Optionsoptions

Collapse all

    Whether to allow creating new items.

    • Default: true

    Custom filter function for items.

    Array of available items.

    Filter function for new items.

    Position of new items in list.

    • Default: 'end'

ReturnsT[]

    Filtered array of selectable items.

useSelectEditorCombobox

Hook for combobox behavior including text cleanup and item selection.

Parameters

Collapse all

    Options for combobox behavior.

Optionsoptions

Collapse all

    Whether combobox is open.

    Function to select first combobox item.

    Callback when selected items change.

Types

TTagElement

type TTagElement = TElement & {
  value: string;
  [key: string]: unknown;
};

TagLike

type TagLike = {
  value: string;
  [key: string]: unknown;
};