From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • Anchor API
  • Location API
  • Path API
  • PointEntry API
  • Point API
  • Range API
  • Selection API
  • Location Types APIs
  • Span API
  • Editor
  • Element API
  • NodeEntry API
  • Node API
  • Node Types APIs
  • Text API
  • Debug Value Scrubbing
  • Transforms API
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • FAQ
  • Resources

Executing Commands

PreviousNext

Extract reusable editor commands without mutating the editor object directly.

Formatting logic can live directly inside event handlers, but it gets repetitive once the same behavior is used from keyboard shortcuts, toolbar buttons, menu items, or tests. A command is reusable editor logic. Keep command reads in editor.read.<group>.<method>() or editor.read(...), and command writes in editor.update.<group>.<method>() or editor.update(...).

Extracting Commands

Start by moving the bold and code-block logic into plain functions:

import { ElementApi, type Editor } from "@platejs/plite";
 
const isBoldActive = (editor: Editor) => {
  return


























Applying Custom FormattingSaving to a Database

On This Page

Extracting CommandsUsing Commands From Editor EventsUsing Commands From UIExtension Commands
Build your editor
Production-ready AI template and reusable components.
Get all-access
editor.read.
marks
()?.bold
===
true
;
};
const isCodeBlockActive = (editor: Editor) => {
const match = editor.read.nodes.find({
match: (node) => ElementApi.isElement(node) && node.type === "code",
});
return Boolean(match);
};
const toggleBold = (editor: Editor) => {
editor.update.marks.toggle("bold");
};
const toggleCodeBlock = (editor: Editor) => {
const isActive = isCodeBlockActive(editor);
editor.update((tx) => {
tx.nodes.set(
{ type: isActive ? "paragraph" : "code" },
{
match: (node) =>
ElementApi.isElement(node) && !tx.schema.isInline(node),
}
);
});
};
import { ElementApi, type Editor } from "@platejs/plite";
 
const isBoldActive = (editor: Editor) => {
  return editor.read.marks()?.bold === true;
};
 
const isCodeBlockActive = (editor: Editor) => {
  const match = editor.read.nodes.find({
    match: (node) => ElementApi.isElement(node) && node.type === "code",
  });
 
  return Boolean(match);
};
 
const toggleBold = (editor: Editor) => {
  editor.update.marks.toggle("bold");
};
 
const toggleCodeBlock = (editor: Editor) => {
  const isActive = isCodeBlockActive(editor);
 
  editor.update((tx) => {
    tx.nodes.set(
      { type: isActive ? "paragraph" : "code" },
      {
        match: (node) =>
          ElementApi.isElement(node) && !tx.schema.isInline(node),
      }
    );
  });
};

These functions are not added to the editor object. They are normal JavaScript functions that receive an editor.

Using Commands From Editor Events

Use Editable onKeyDown for keyboard shortcuts that belong to one editor UI:

import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        onKeyDown={(event, { editor }) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
            toggleCodeBlock(editor);
            return true;
          }
 
          if (event.key === "b" && event.ctrlKey) {
            event.preventDefault();
            toggleBold(editor);
            return true;
          }
        }}
        renderElement={renderElement}
        renderLeaf={renderLeaf}
      />
    </Plite>
  );
};
import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        onKeyDown={(event, { editor }) => {
          if (event.key === "`" && event.ctrlKey) {
            event.preventDefault();
            toggleCodeBlock(editor);
            return true;
          }
 
          if (event.key === "b"










Use extension commands for reusable semantic behavior. This keeps the same model rule available to keyboard input, native input, toolbar logic, programmatic calls, and tests.

import {
  defineExtension,
  editorCommands,
  ElementApi,
  PointApi,
  RangeApi,
} from "@platejs/plite";
 
const markdownBlocks = defineExtension("markdown-blocks", {
  commands: ({ around, handle }) => [
    handle(editorCommands.delete, ({ input, state }) => {
      const selection = state.selection();
 
      if (
        input.direction === "backward" &&
        input.unit === "character" &&
        selection &&
        RangeApi.isCollapsed(selection)
      ) {
        const blockEntry = state.nodes.block({ at: selection });
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry;
          const start = state.points.start(blockPath);
 
          if (
            ElementApi.isElement(block) &&
            block.type !== "paragraph" &&
            PointApi.equals(selection.anchor, start)
          ) {
            return state.transaction((tx) => {
              tx.blocks.set({ type: "paragraph" }, { at: blockPath });
            });
          }
        }
      }
 
      return false;
    }),
    around(editorCommands.insertBreak, ({ next, state }) => {
      const selection = state.selection();
 
      if (selection && RangeApi.isCollapsed(selection)) {
        const blockEntry = state.nodes.block({ at: selection });
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry;
 
          if (ElementApi.isElement(block) && block.type === "heading-one") {
            const start = state.points.start(blockPath);
 
            if (PointApi.equals(selection.anchor, start)) {
              return next.after(
                state.transaction((tx) => {
                  tx.blocks.set({ type: "paragraph" }, { at: blockPath });
                })
              );
            }
          }
        }
      }
 
      return next();
    }),
  ],
});
 
const App = () => {
  const editor = usePliteEditor({
    extensions: [markdownBlocks],
    initialValue,
  });
 
  return (
    <Plite editor={editor}>
      <Editable renderElement={renderElement} renderLeaf={renderLeaf} />
    </Plite>
  );
};
import {
  defineExtension,
  editorCommands,
  ElementApi,
  PointApi,
  RangeApi,
} from "@platejs/plite";
 
const markdownBlocks = defineExtension("markdown-blocks", {
  commands: ({ around, handle }) => [
    handle(editorCommands.delete, ({ input, state }) => {
      const selection = state.selection();
 
      if (
        input.direction === "backward" &&
        input.unit === "character" &&
        selection &&





























































Using Commands From UI

The same functions can be called from toolbar buttons:

const Toolbar = ({ editor }) => {
  return (
    <div>
      <button
        onMouseDown={(event) => {
          event.preventDefault();
          toggleBold(editor);
        }}
      >
        Bold
      </button>
      <button
        onMouseDown={(event) => {
          event.preventDefault();
          toggleCodeBlock(editor);
        }}
      >
        Code Block
      </button>
    </div>
  );
};
const Toolbar = ({ editor }) => {
  return (
    <div>
      <button
        onMouseDown={(event) => {
          event.preventDefault();
          toggleBold(editor);
        }}
      >
        Bold
      </button>
      <button
        onMouseDown={(event) => {
          event.preventDefault();
          toggleCodeBlock(editor);
        }}
      >
        Code Block
      </


Extension Commands

Plain functions are enough for app code. Extensions can expose typed state and tx namespaces when a behavior needs to be shared across editors.

Raw Plite does not ship product commands like lists, headings, or links. Those belong in extensions or higher-level frameworks.

&&
event.ctrlKey) {
event.preventDefault();
toggleBold(editor);
return true;
}
}}
renderElement={renderElement}
renderLeaf={renderLeaf}
/>
</Plite>
);
};
RangeApi.isCollapsed(selection)
) {
const blockEntry = state.nodes.block({ at: selection });
if (blockEntry) {
const [block, blockPath] = blockEntry;
const start = state.points.start(blockPath);
if (
ElementApi.isElement(block) &&
block.type !== "paragraph" &&
PointApi.equals(selection.anchor, start)
) {
return state.transaction((tx) => {
tx.blocks.set({ type: "paragraph" }, { at: blockPath });
});
}
}
}
return false;
}),
around(editorCommands.insertBreak, ({ next, state }) => {
const selection = state.selection();
if (selection && RangeApi.isCollapsed(selection)) {
const blockEntry = state.nodes.block({ at: selection });
if (blockEntry) {
const [block, blockPath] = blockEntry;
if (ElementApi.isElement(block) && block.type === "heading-one") {
const start = state.points.start(blockPath);
if (PointApi.equals(selection.anchor, start)) {
return next.after(
state.transaction((tx) => {
tx.blocks.set({ type: "paragraph" }, { at: blockPath });
})
);
}
}
}
}
return next();
}),
],
});
const App = () => {
const editor = usePliteEditor({
extensions: [markdownBlocks],
initialValue,
});
return (
<Plite editor={editor}>
<Editable renderElement={renderElement} renderLeaf={renderLeaf} />
</Plite>
);
};
button
>
</div>
);
};