Skip to content
D
Documentation

Make edits undoable

how-to
2 min readUpdated

Use the mounted diagram's history for both user gestures and edits that your UI applies. A drag is one history step; Ctrl+Z or ⌘Z restores the previous state, and redo reapplies it.

When to use this

Use this pattern when a toolbar, assistant, or other application action changes a diagram on the user's behalf. Build the diagram through the binding, keep the returned DiagramInstance, and call the instance's engine for programmatic history operations. Do not mutate the model directly for an edit that the user must undo.

The engine's addNode() method executes an add-node command, so the new node enters the same history as a pointer edit. undo() and redo() are asynchronous; await them before reading the restored model or updating related UI.

Add undo and redo controls

The following examples mount two connected nodes, expose buttons, and keep the diagram container sized. Drag either node, then choose Undo: the node returns to its starting position. Choose Redo to restore the dropped position.

js
import { render } from '@grafloria/element';

const target = document.getElementById('diagram');
if (!target) {
  throw new Error('Missing #diagram');
}
target.style.height = '400px';

const instance = render({
  nodes: [
    { id: 'a', position: { x: 160, y: 160 }, size: { width: 160, height: 70 }, label: 'Drag, then undo' },
    { id: 'b', position: { x: 480, y: 280 }, size: { width: 160, height: 70 }, label: 'Every step counts' },
  ],
  edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, target);

const engine = instance.getEngine();
const undoButton = document.getElementById('undo');
const redoButton = document.getElementById('redo');
const addButton = document.getElementById('add');
if (!undoButton || !redoButton || !addButton) {
  throw new Error('Missing history controls');
}

undoButton.addEventListener('click', () => { void engine.undo(); });
redoButton.addEventListener('click', () => { void engine.redo(); });
addButton.addEventListener('click', async () => {
  await engine.addNode({
    type: 'default',
    position: { x: 160, y: 360 },
    size: { width: 160, height: 70 },
  });
});

In JavaScript, add a control for the programmatic edit beside the diagram:

html
<button id="add">Add node</button>
<button id="undo">Undo</button>
<button id="redo">Redo</button>
<div id="diagram" style="height:400px"></div>

Click Add node, then Undo. The added node disappears from the mounted diagram; Redo puts it back. render() returns the live instance, getEngine() reaches the shared engine, and addNode() records its command before undo() runs it in reverse.

Keep history state in sync

Use canUndo() and canRedo() to disable toolbar buttons when their stacks are empty. The Angular component checks these states internally and schedules a render after each operation; React, Vue, Qwik, and JavaScript reach the engine through the instance, as in the examples above. Await an operation when a subsequent action depends on its result.

CallReturnsUse
engine.undo()Promise<void>Restore the last command.
engine.redo()Promise<void>Reapply the last undone command.
engine.canUndo()booleanSet the Undo button's disabled state.
engine.canRedo()booleanSet the Redo button's disabled state.
Angular undo() / redo()Promise<void>Use the component surface; it also schedules the component render.

Pitfalls

  • Do not call diagram.addNode() for a user-facing edit. Direct model loading is not a history entry; use engine.addNode() or another command-backed engine operation.
  • Do not treat undo() and redo() as synchronous. The engine methods return promises, and the Angular component also exposes promises.
  • Give the mounted element a height. A diagram in a zero-height container has no visible drawing area.
  • A completed node drag is one undo step, not one step per pointer movement. A drag that ends where it started adds nothing to history.

See it running

Open the undo/redo demo to see two mounted diagrams share edits while each peer undoes its own operation.

Two canvases converge after Undo and Redo while the node positions remain visible.

See Commands and undo for command composition and Instance and reconciliation for the live instance surface.

Was this page helpful?