Learn / Commands & undo

Commands & undo

Every user gesture — drag, connect, delete, paste, group — becomes a command object on one history stack. That is why ⌘Z works everywhere with zero wiring, and why your own features can join the same history.

The free part

Mount any canvas and ⌘Z/Ctrl+Z undoes, ⌘⇧Z/Ctrl+Y redoes. One drag is one step — not sixty position updates. Buttons are one call:

await engine.undo();      // all async — a command may do async work
await engine.redo();
engine.canUndo();          // drive your buttons' disabled state
engine.canRedo();

Framework surfaces: Angular's canvas mirrors undo()/redo() as component methods; in React and Vue you reach the engine via the instance — instance.getEngine().undo().

Live: drag something, press ⌘Z →

Programmatic edits: two intents, two APIs

This distinction is deliberate and worth internalizing:

You wantUseHistory
To load or build a diagram (setup, import, sync) diagram.addNode(node), diagram.addLink(link) not recorded — loading a file should not be undoable
To edit on the user's behalf (a toolbar action, an AI suggestion) engine.commandManager.execute(command) recorded — the user can ⌘Z your feature
import { AddNodeCommand } from '@grafloria/engine';

const node = new NodeModel({ id: 'ship', type: 'task',
  position: { x: 480, y: 60 }, size: { width: 120, height: 48 } });

await engine.commandManager.execute(new AddNodeCommand(node));
engine.canUndo();     // true — your edit is a first-class history entry
await engine.undo();  // and it cleanly reverts

The command vocabulary

The built-in commands cover the whole editing surface — AddNodeCommand, RemoveNodeCommand, AddLinkCommand, MoveNodeCommand, AlignCommand, AddGroupCommand, CollapseGroupCommand, BringNodeToFrontCommand, and more. Two are structural:

  • BatchCommand / MacroCommand — bundle several commands into one undo step ("paste 12 nodes" is one ⌘Z, not twelve).
  • Your own — extend Command with execute(context) / undo(context) and your domain action rides the same stack as everything else.
A command can refuse: canExecute(context) returning false means the command never enters history at all — a refused action leaves nothing to "undo". Real-time validation composes with this: in strict mode, a command whose result fails validation is reverted and never recorded.

How the frameworks fold history back into state

After an undo, the engine's models changed — the bindings re-emit them: React's onNodesChange fires with the reverted NodeModel[], Angular's [(nodes)] writes back, Vue's v-model:nodes updates. Your state and the history never disagree, because there is only one history — the engine's.

Where next