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

Editor

PreviousNext

Use the Editor runtime for document meta, selection, canonical changes, schema, extensions, and subscriptions.

The Editor object is the runtime for one Plite document. It owns the document value, selection, canonical changes, schema behavior, extensions, and subscriptions.

Most application code touches the editor in three ways:

  • read committed state with editor.read(...) or editor.read.<group>.<method>()
  • write through editor.update(...) or editor.update.<group>.<method>()
  • install reusable behavior through createEditor({ extensions })
  • publish host-selected runtime behavior through editor.install(...)

Reading State

Use direct read methods for one-shot reads:

const

CommandsExtensions

On This Page

Reading StateWriting StateSnapshots And CommitsUpdate PoliciesPreserving RangesExtending The EditorSchema BehaviorQuery Groups
Build your editor
Production-ready AI template and reusable components.
Get all-access
selection
=
editor.read.
selection
();
const text = editor.read.text.string([]);
const value = editor.read.value();
const selection = editor.read.selection();
const text = editor.read.text.string([]);
const value = editor.read.value();

Use the callback form when several reads should share one consistent state view. Plite passes a grouped state object into the callback.

const info = editor.read((state) => {
  return {
    selection: state.selection(),
    text: state.text.string([]),
    value: state.value(),
  };
});
const info = editor.read((state) => {
  return {
    selection: state.selection(),
    text: state.text.string([]),
    value: state.value(),
  };
});

The callback is read-only. Starting a write from inside a read is rejected because it would mix two different editor snapshots.

Writing State

Use direct update methods for one-shot writes:

editor.update.text.insert("!");
editor.update.nodes.set({ type: "heading-one" }, { at: [0] });
editor.update.selection.set(editor.read.points.get([]));
editor.update({ history: "skip" }).value.replace({
  children: [{ type: "paragraph", children: [{ text: "Reset" }] }],
  selection: "end",
});
editor.update.text.insert("!");
editor.update.nodes.set({ type: "heading-one" }, { at: [0] });
editor.update.selection.set(editor.read.points.get([]));
editor.update({ history: "skip" }).value.replace({
  children: [{ type: "paragraph", children: [{ text: "Reset" }] }],
  selection: "end",
});

Use the callback form when one command groups related writes into a single commit. Plite passes a transaction object into the callback.

editor.update((tx) => {
  tx.text.insert("!");
  tx.nodes.set({ type: "heading-one" }, { at: [0] });
  tx.selection.set(tx.points.end([]));
});
editor.update((tx) => {
  tx.text.insert("!");
  tx.nodes.set({ type: "heading-one" }, { at: [0] });
  tx.selection.set(tx.points.end([]));
});

All writes in the callback become one commit. That gives history, change replay, sync adapters, and React rendering one consistent change to observe.

Pass a policy first when the whole update needs history or lifecycle tags:

editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
  tx.slice.replace(importedSlice);
  tx.selection.collapse({ edge: "end" });
});
editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
  tx.slice.replace(importedSlice);
  tx.selection.collapse({ edge: "end" });
});

Update callbacks are synchronous. Use the active tx for every nested command; starting another public editor.update(...) inside the callback is rejected and rolls back the outer update.

Snapshots And Commits

Plite exposes committed state through subscriptions. Most application reads should use narrow state groups like state.value, state.selection, and extension-owned state.

const unsubscribe = editor.subscribe((_snapshot, change) => {
  if (change?.changed.has("document") || change?.dirtyStateKeys.length) {
    const documentValue = editor.read.value();
 
    saveDocument(documentValue);
  }
});
const unsubscribe = editor.subscribe((_snapshot, change) => {
  if (change?.changed.has("document") || change?.dirtyStateKeys.length) {
    const documentValue = editor.read.value();
 
    saveDocument(documentValue);
  }
});

Use subscriptions for app services that need to observe commits. Use React hooks from @platejs/plite-react for UI that renders editor state.

Full snapshots are runtime observer data. Use them for debug and test tooling that intentionally needs the whole document, selection, compiled path index, and node-key index. Pending insertion marks belong to collapsed text selections, not to a parallel snapshot field.

const snapshot = editor.read((state) => state.runtime.snapshot());
const snapshot = editor.read((state) => state.runtime.snapshot());

Update Policies

EditorUpdatePolicy has two public fields: history and tags. History modes are "merge", "new-batch", and "skip"; TypeScript exposes them only when the editor has a History transaction group. Tags are ordered lifecycle labels recorded on the commit.

editor.update({ tags: ["paste", "import"] }, (tx) => {
  tx.text.insert(importedText);
});
editor.update({ tags: ["paste", "import"] }, (tx) => {
  tx.text.insert(importedText);
});

Policy tags are applied in order, then the semantic history mode is applied. Only the last history mode survives. Inside an atomic update, tx.tags.add(tag) can replace that mode or add another label, and tx.tags.has(tag) inspects the current final set.

Framework and feature packages own presets for their behavior. For example, PliteReactUpdatePolicy.preserveSelection, YjsUpdatePolicy.remote, and SuggestionUpdatePolicy.skip add the exact tags their runtimes consume. Plite core does not know those product policies.

Use state fields without a persist codec for local provenance UI that should follow the runtime but stay out of saved document JSON. Runtime ids are useful for local projection and debug links; semantic product ids belong in your own model when they need to persist.

Preserving Ranges

Use anchors when you need a local range to survive document edits.

const selection = editor.read.selection();
const anchor = selection
  ? editor.anchor(selection, {
      association: "inward",
      deletion: "drop",
    })
  : null;
 
editor.update((tx) => {
  tx.nodes.unwrap();
 
  const selection = anchor?.release();
 
  if (selection) {
    tx.selection.set(selection);
  }
});
const selection = editor.read.selection();
const anchor = selection
  ? editor.anchor(selection, {
      association: "inward",
      deletion: "drop",
    })
  : null;
 
editor.update((tx) => {
  tx.nodes.unwrap();
 
  const selection = anchor?.release();
 
  if (selection) {
    tx.selection.set(selection);
  }
});

Anchors are local runtime values. Store shared document meta as document values, canonical changes, effects, and commits. Use Document Meta for values that need to persist with the document.

Extending The Editor

Extensions package reusable behavior without mutating random fields onto the editor object. They can register owner-local read and update namespaces, schema contributions, commit listeners, changed-range corrections, effects, and optional runtime services.

Here's a small extension that adds a table namespace:

import { createEditor, defineExtension } from "@platejs/plite";
 
const TablesExtension = defineExtension("tables", {
  read: ({ state }) => ({
    rowCount() {
      return state.nodes.children().length;
    },
  }),
  update: ({ tx }) => ({
    insertRow(text = "row") {
      tx.nodes.insert(
        {
          type: "paragraph",
          children: [{ text }],
        },
        { at: [tx.nodes.children().length] }
      );
    },
  }),
});
 
const editor = createEditor({ extensions: [TablesExtension] as const });
import { createEditor, defineExtension } from "@platejs/plite";
 
const TablesExtension = defineExtension("tables", {
  read: ({ state }) => ({
    rowCount() {
      return state.nodes.children().length;
    },
  }),
  update: ({ tx }) => ({
    insertRow(text = "row") {
      tx.nodes.insert(
        {
          type: "paragraph",
          children: [{ text }],
        },
        { at: [tx.nodes.children().length





The extension adds helpers to grouped reads, atomic updates, and direct update methods.

const rows = editor.read((state) => state.tables.rowCount());
 
editor.update.tables.insertRow();
 
editor.update((tx) => {
  tx.tables.insertRow();
});
const rows = editor.read((state) => state.tables.rowCount());
 
editor.update.tables.insertRow();
 
editor.update((tx) => {
  tx.tables.insertRow();
});

Extension authors can mark a method with txOnly(...) when it only makes sense inside an active transaction. Those methods stay on the transaction namespace and are omitted from the direct update surface.

Schema Behavior

Schema checks live on the read and transaction views. Use element specs and extension-owned schema policy to decide how Plite treats your node types.

For example, image elements can be treated as block voids, and mention elements can be treated as inline markable voids. The React renderer uses those schema facts to render the correct DOM shell.

Query Groups

Direct read methods cover common one-shot queries. The read callback exposes the grouped helpers when code needs a custom query or an extension-owned namespace.

const point = editor.read.points.start([0, 0]);
const text = editor.read.text.string(range);
 
for (const [node, path] of editor.read((state) =>
  state.nodes.entries({ at: range })
)) {
  // ...
}
const point = editor.read.points.start([0, 0]);
const text = editor.read.text.string(range);
 
for (const [node, path] of editor.read((state) =>
  state.nodes.entries({ at: range })
)) {
  // ...
}

These helpers are useful inside commands, renderers, and app services. Keep document writes inside editor.update(...).

] }
);
},
}),
});
const editor = createEditor({ extensions: [TablesExtension] as const });