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 want | Use | History |
|---|---|---|
| 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
Commandwithexecute(context)/undo(context)and your domain action rides the same stack as everything else.
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
- The model — what these commands mutate.
- Collaboration — how local commands become CRDT ops.