Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • HTML Export
  • Export
  • Server-Side
  • Version History
  • Editable Voids
  • Huge Document
  • Hundreds Editors
  • Markdown Streaming
  • Preview Markdown
  • Collaboration Demo
  • Table Nomerge Demo
  • Excalidraw Demo
  • Code Drawing Demo
  • Single Block Demo
  • Find Demo
  • Lint Demo
  • AI Demo
  • Align Demo
  • Autoformat Demo
  • Basic Nodes Demo
  • Block Menu Demo
  • Node Selection Demo
  • Column Demo
  • Code Block Demo
  • Callout Demo
  • Discussion Demo
  • Date Demo
  • Footnote Demo
  • Drag & Drop Demo
  • Emoji Demo
  • Equation Demo
  • Exit Break Demo
  • Floating Toolbar Demo
  • Font Demo
  • Indent Demo
  • List Demo
  • Line Height Demo
  • Link Demo
  • Media Demo
  • Mention Demo
  • Block Placeholder Demo
  • Serializing Docx Demo
  • Serializing HTML Demo
  • Serializing Markdown Demo
  • Slash Command Demo
  • Plugin Rules Demo
  • Table Demo
  • Table of Contents Demo
  • Details Demo

Version History

PreviousNext

Retain author history and compare saved document revisions.

This example installs native authored changes with retainHistory: true. Edit as Alice or Bob, revert an accepted contribution, or save two revisions to inspect their structural comparison.

For application revision previews and restores with comments, see Comments and versions.

Demo

Loading…
Server-SideEditable Voids Demo

On This Page

DemoSourceUsageRetained history flowCompare saved revisionsPersist the history
Build your editor
Production-ready AI template and reusable components.
Get all-access

Source

'use client';
 
import type { EditorStateSchemaApi, Value } from 'platejs';
import { compare, type TwoWayComparison } from 'platejs/diff';
import {
  EditorRoot,
  EditorContent,
  useCreateEditor,
  useEditor,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import { BasicMarksKit } from '@/components/editor/basic-marks';
import { Diff } from '@/components/editor/diff';
 
type Revision = ReturnType<ReturnType<typeof useEditor>['read']['value']>;
 
const initialValue: Value = [
  {
    children: [{ text: 'Save a revision after each edit to compare them.' }],
    type: 'paragraph',
  },
  {
    children: [{ text: 'Edit this text, then save a revision.' }],
    type: 'paragraph',
  },
];
 
export function VersionDiff({
  after,
  before,
  schema,
}: {
  after: unknown;
  before: unknown;
  schema: EditorStateSchemaApi;
}) {
  const [result, setResult] = React.useState<{
    after: unknown;
    comparison: TwoWayComparison | null;
    error: string | null;
  } | null>(null);
 
  React.useEffect(() => {
    const controller = new AbortController();
 
    void compare({ after, before, schema, signal: controller.signal }).then(
      (comparison) => {
        if (!controller.signal.aborted) {
          setResult({ after, comparison, error: null });
        }
      },
      (error: unknown) => {
        if (
          !controller.signal.aborted &&
          !(error instanceof Error && error.name === 'AbortError')
        ) {
          setResult({
            after,
            comparison: null,
            error:
              error instanceof Error ? error.message : 'Comparison failed.',
          });
        }
      }
    );
 
    return () => controller.abort();
  }, [after, before, schema]);
 
  if (result && result.after === after && result.error) {
    return (
      <p className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-950">
        {result.error}
      </p>
    );
  }
  if (!result || result.after !== after || !result.comparison) {
    return (
      <p aria-live="polite" className="rounded-md border p-3 text-sm">
        Comparing revisions…
      </p>
    );
  }
 
  return <Diff comparison={result.comparison} />;
}
 
function RevisionHistory({
  onSave,
  revisions,
}: {
  onSave: (revision: Revision) => void;
  revisions: readonly Revision[];
}) {
  const editor = useEditor();
 
  return (
    <section className="space-y-2">
      <div className="flex items-center justify-between gap-3">
        <h2 className="font-medium">Saved revisions</h2>
        <Button
          onClick={() => onSave(structuredClone(editor.read.value()))}
          size="sm"
          variant="outline"
        >
          Save revision
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Edit the document and save a revision to compare it with the previous
        snapshot, or restore any saved revision.
      </p>
      <ol className="space-y-1 text-sm">
        {revisions.map((revision, index) => (
          <li
            className="flex items-center justify-between gap-3"
            // Revisions are append-only, so a position names one revision.
            key={index}
          >
            <span>Revision {index + 1}</span>
            <Button
              aria-label={`Restore revision ${index + 1}`}
              onClick={() => editor.update((tx) => tx.value.replace(revision))}
              size="sm"
              variant="ghost"
            >
              Restore
            </Button>
          </li>
        ))}
      </ol>
      {revisions.length > 1 && (
        <VersionDiff
          after={revisions.at(-1)}
          before={revisions.at(-2)}
          schema={editor.read.schema}
        />
      )}
    </section>
  );
}
 
export default function VersionHistoryDemo() {
  const editor = useCreateEditor({
    plugins: BasicMarksKit,
    initialValue,
  });
  const [revisions, setRevisions] = React.useState<readonly Revision[]>(() => [
    structuredClone(editor.read.value()),
  ]);
 
  return (
    <div className="flex flex-col gap-4 p-3">
      <EditorRoot editor={editor}>
        <EditorContent className="rounded-md border p-3" />
        <RevisionHistory
          onSave={(revision) =>
            setRevisions((current) => [...current, revision])
          }
          revisions={revisions}
        />
      </EditorRoot>
    </div>
  );
}
'use client';
 
import type { EditorStateSchemaApi, Value } from 'platejs';
import { compare, type TwoWayComparison } from 'platejs/diff';
import {
  EditorRoot,
  EditorContent,
  useCreateEditor,
  useEditor,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import { BasicMarksKit } from '@/components/editor/basic-marks';
import { Diff } from '@/components/editor/diff';
 
type Revision =





















































































































































Usage

PieceOwnerRole
AuthoredPlugin.configure({ initialState: { retainHistory: true } })platejs/authoredCaptures accepted edits with stable author and change identities and retains their edit bodies. The editor reads the option once, when it is created.
userIdapplicationNames the author of each accepted edit; an editor without one writes as the local user.
read.authored.changes(...)native authored readsReturns accepted history as cursor-paged AuthoredChange records.
update.authored.revert(...)native authored updatesCreates a compensating contribution for the selected retained change.
compare(...)platejs/diffCompares two immutable saved revisions without changing the editor.
Diffcopied Plate UIShows comparison groups, effects, filters, and navigation.
BasicMarksKitcopied Plate UIProvides normal rich-text marks for the editable document.

Retained history flow

  1. The editor starts with authored and retainHistory: true.
  2. The author selector changes the identity returned for later transactions.
  3. Ordinary accepted edits create attributed records.
  4. The history list reads accepted changes by stable ID and revision.
  5. Revert captures a snapshot-bound selection and calls update.authored.revert(...).
  6. The new compensating change belongs to the current author; the original record remains intact.

A revert can be blocked by dependent work or become unavailable when the required retained content has expired. Local undo and redo remain separate interaction history. Use native decide(...) to accept or reject pending review proposals.

Compare saved revisions

The demo captures an initial snapshot. Edit the document and select Save revision to compare the two latest snapshots. Saving another revision starts a new comparison; an obsolete comparison is cancelled. The comparison is read-only. To turn a validated comparison into a pending proposal, use proposeAuthoredComparison(...).

Persist the history

Save editor.read.value() as one document. The authored envelope includes accepted content, causal records, decisions, and the retained bodies selected by policy.

const saved = editor.read.value();
 
const reloaded = createEditor({
  plugins: [
    AuthoredPlugin.configure({ initialState: { retainHistory: true } }),
  ],
  initialValue: saved,
  userId: session.user.id,
});
const saved = editor.read.value();
 
const reloaded = createEditor({
  plugins: [
    AuthoredPlugin.configure({ initialState: { retainHistory: true } }),
  ],
  initialValue: saved,
  userId: session.user.id,
});

The fresh editor starts with empty local undo and redo stacks. Retained authored contributions remain in the saved document. If the document has comments, save CommentsJSON through comments.toJSON() with the same revision and load it through initialComments; document undo does not rewind the discussion.

See Authored Changes for review views, decision results, collaboration, and format projections.

ReturnType
<
ReturnType
<
typeof
useEditor>[
'read'
][
'value'
]>;
const initialValue: Value = [
{
children: [{ text: 'Save a revision after each edit to compare them.' }],
type: 'paragraph',
},
{
children: [{ text: 'Edit this text, then save a revision.' }],
type: 'paragraph',
},
];
export function VersionDiff({
after,
before,
schema,
}: {
after: unknown;
before: unknown;
schema: EditorStateSchemaApi;
}) {
const [result, setResult] = React.useState<{
after: unknown;
comparison: TwoWayComparison | null;
error: string | null;
} | null>(null);
React.useEffect(() => {
const controller = new AbortController();
void compare({ after, before, schema, signal: controller.signal }).then(
(comparison) => {
if (!controller.signal.aborted) {
setResult({ after, comparison, error: null });
}
},
(error: unknown) => {
if (
!controller.signal.aborted &&
!(error instanceof Error && error.name === 'AbortError')
) {
setResult({
after,
comparison: null,
error:
error instanceof Error ? error.message : 'Comparison failed.',
});
}
}
);
return () => controller.abort();
}, [after, before, schema]);
if (result && result.after === after && result.error) {
return (
<p className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-950">
{result.error}
</p>
);
}
if (!result || result.after !== after || !result.comparison) {
return (
<p aria-live="polite" className="rounded-md border p-3 text-sm">
Comparing revisions…
</p>
);
}
return <Diff comparison={result.comparison} />;
}
function RevisionHistory({
onSave,
revisions,
}: {
onSave: (revision: Revision) => void;
revisions: readonly Revision[];
}) {
const editor = useEditor();
return (
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<h2 className="font-medium">Saved revisions</h2>
<Button
onClick={() => onSave(structuredClone(editor.read.value()))}
size="sm"
variant="outline"
>
Save revision
</Button>
</div>
<p className="text-sm text-muted-foreground">
Edit the document and save a revision to compare it with the previous
snapshot, or restore any saved revision.
</p>
<ol className="space-y-1 text-sm">
{revisions.map((revision, index) => (
<li
className="flex items-center justify-between gap-3"
// Revisions are append-only, so a position names one revision.
key={index}
>
<span>Revision {index + 1}</span>
<Button
aria-label={`Restore revision ${index + 1}`}
onClick={() => editor.update((tx) => tx.value.replace(revision))}
size="sm"
variant="ghost"
>
Restore
</Button>
</li>
))}
</ol>
{revisions.length > 1 && (
<VersionDiff
after={revisions.at(-1)}
before={revisions.at(-2)}
schema={editor.read.schema}
/>
)}
</section>
);
}
export default function VersionHistoryDemo() {
const editor = useCreateEditor({
plugins: BasicMarksKit,
initialValue,
});
const [revisions, setRevisions] = React.useState<readonly Revision[]>(() => [
structuredClone(editor.read.value()),
]);
return (
<div className="flex flex-col gap-4 p-3">
<EditorRoot editor={editor}>
<EditorContent className="rounded-md border p-3" />
<RevisionHistory
onSave={(revision) =>
setRevisions((current) => [...current, revision])
}
revisions={revisions}
/>
</EditorRoot>
</div>
);
}