Editable Voids

PreviousNext

Nest editable controls and a Plate editor inside a void element.

This example renders form controls and a nested Plate editor inside a void element. It is app-local demo code built with createPlatePlugin, not a packaged feature plugin.

Demo

Loading…

Source

The demo registers an editable-void element plugin and renders custom React UI for that node.

'use client';
 
import * as React from 'react';
 
import type { PlateElementProps } from 'platejs/react';
 
import { createPlatePlugin, Plate, usePlateEditor } from 'platejs/react';
 
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { EditorKit } from '@/components/editor/editor-kit';
import { editableVoidsValue } from '@/registry/examples/values/editable-voids-value';
import { Editor, EditorContainer } from '@/components/ui/editor';
 
export const EditableVoidPlugin = createPlatePlugin({
  key: 'editable-void',
  node: {
    component: EditableVoidElement,
    isElement: true,
    isVoid: true,
  },
});
 
export function EditableVoidElement({
  attributes,
  children,
}: PlateElementProps) {
  const [inputValue, setInputValue] = React.useState('');
 
  const editor = usePlateEditor({
    plugins: EditorKit,
  });
 
  return (
    // Need contentEditable=false or Firefox has issues with certain input types.
    <div {...attributes} contentEditable={false}>
      <div className="mt-2 grid gap-6 rounded-md border p-6 shadow-sm">
        <Input
          id="name"
          className="my-2"
          value={inputValue}
          onChange={(e) => {
            setInputValue(e.target.value);
          }}
          placeholder="Name"
          type="text"
        />
 
        <div className="grid w-full max-w-sm items-center gap-2">
          <Label htmlFor="handed">Left or right handed:</Label>
 
          <RadioGroup id="handed" defaultValue="r1">
            <div className="flex items-center space-x-2">
              <RadioGroupItem id="r1" value="r1" />
              <Label htmlFor="r1">Left</Label>
            </div>
            <div className="flex items-center space-x-2">
              <RadioGroupItem id="r2" value="r2" />
              <Label htmlFor="r2">Right</Label>
            </div>
          </RadioGroup>
        </div>
 
        <div className="grid gap-2">
          <Label htmlFor="editable-void-basic-blocks">
            Tell us about yourself:
          </Label>
 
          <Plate
            editor={editor}
            // initialValue={basicBlocksValue}
          >
            <EditorContainer>
              <Editor />
            </EditorContainer>
          </Plate>
        </div>
      </div>
      {children}
    </div>
  );
}
 
export default function EditableVoidsDemo() {
  const editor = usePlateEditor({
    plugins: [...EditorKit, EditableVoidPlugin],
    value: editableVoidsValue,
  });
 
  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
'use client';
 
import * as React from 'react';
 
import type { PlateElementProps } from 'platejs/react';
 
import { createPlatePlugin, Plate, usePlateEditor } from 'platejs/react';
 
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { EditorKit } from '@/components/editor/editor-kit';
import { editableVoidsValue } from '@/registry/examples/values/editable-voids-value';
import { Editor, EditorContainer } from '@/components/ui/editor';
 
export const EditableVoidPlugin = createPlatePlugin({
  key: 'editable-void',
  node: {
    component: EditableVoidElement,
    isElement: true,
    isVoid: true,
  },
});
 
export function EditableVoidElement({
  attributes,
  children,
}: PlateElementProps) {
  const [inputValue, setInputValue] = React.useState('');
 
  const editor = usePlateEditor({
    plugins: EditorKit,
  });
 
  return (
    // Need contentEditable=false or Firefox has issues with certain input types.
    <div {...attributes} contentEditable={false}>
      <div className="mt-2 grid gap-6 rounded-md border p-6 shadow-sm">
        <Input
          id="name"
          className="my-2"
          value={inputValue}
          onChange={(e) => {
            setInputValue(e.target.value);
          }}
          placeholder="Name"
          type="text"
        />
 
        <div className="grid w-full max-w-sm items-center gap-2">
          <Label htmlFor="handed">Left or right handed:</Label>
 
          <RadioGroup id="handed" defaultValue="r1">
            <div className="flex items-center space-x-2">
              <RadioGroupItem id="r1" value="r1" />
              <Label htmlFor="r1">Left</Label>
            </div>
            <div className="flex items-center space-x-2">
              <RadioGroupItem id="r2" value="r2" />
              <Label htmlFor="r2">Right</Label>
            </div>
          </RadioGroup>
        </div>
 
        <div className="grid gap-2">
          <Label htmlFor="editable-void-basic-blocks">
            Tell us about yourself:
          </Label>
 
          <Plate
            editor={editor}
            // initialValue={basicBlocksValue}
          >
            <EditorContainer>
              <Editor />
            </EditorContainer>
          </Plate>
        </div>
      </div>
      {children}
    </div>
  );
}
 
export default function EditableVoidsDemo() {
  const editor = usePlateEditor({
    plugins: [...EditorKit, EditableVoidPlugin],
    value: editableVoidsValue,
  });
 
  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

The initial value contains a normal paragraph, one editable-void element, and an empty paragraph after it.

/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@platejs/test-utils';
 
jsx;
 
export const editableVoidsValue: any = (
  <fragment>
    <hp>
      In addition to nodes that contain editable text, you can insert void
      nodes, which can also contain editable elements, inputs, or an entire
      other Slate editor.
    </hp>
    <element type="editable-void">
      <htext />
    </element>
    <hp>
      <htext />
    </hp>
  </fragment>
);
/** @jsxRuntime classic */
/** @jsx jsx */
import { jsx } from '@platejs/test-utils';
 
jsx;
 
export const editableVoidsValue: any = (
  <fragment>
    <hp>
      In addition to nodes that contain editable text, you can insert void
      nodes, which can also contain editable elements, inputs, or an entire
      other Slate editor.
    </hp>
    <element type="editable-void">
      <htext />
    </element>
    <hp>
      <htext />
    </hp>
  </fragment>
);

Runtime Shape

PieceOwnerNotes
EditableVoidPluginRegistry exampleUses createPlatePlugin with node.isElement: true and node.isVoid: true.
EditableVoidElementRegistry exampleRenders inputs, radio controls, and a nested <Plate> instance.
Outer editorRegistry exampleLoads EditorKit plus EditableVoidPlugin and the editableVoidsValue.
Inner editorRegistry exampleCreates a separate usePlateEditor({ plugins: EditorKit }) inside the void element.
Value shapeSlateKeeps an empty text child inside the void node so Slate can select it.

Void Element Rules

Set contentEditable={false} on the custom void wrapper. Without it, browser editing behavior can leak into the nested controls; the demo notes Firefox input issues specifically.

Render {children} after the non-editable UI. Slate still needs the hidden void child mounted even when your visible element is fully custom React.

The nested editor is a separate Plate editor. It does not share the outer editor's value, selection, plugins, or undo history.