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.
jsimport { 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 },
});
});
tsimport { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div style="display:flex;gap:8px;padding:12px">
<button (click)="undo()">Undo</button>
<button (click)="redo()">Redo</button>
</div>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:400px" />
`,
})
export class UndoableDiagramComponent {
canvas = viewChild.required(DiagramCanvasComponent);
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' }];
undo(): void { void this.canvas().undo(); }
redo(): void { void this.canvas().redo(); }
}
tsximport { component$, $, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
const 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' },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
export default component$(() => {
const inst = useSignal();
return (
<div style={{ height: '400px' }}>
<button onClick$={async () => { await inst.value?.getEngine().undo(); }}>Undo</button>
<button onClick$={async () => { await inst.value?.getEngine().redo(); }}>Redo</button>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
onInit$={$((instance: DiagramInstance) => { inst.value = instance; })} />
</div>
);
});
tsximport { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
const 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' },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
export default function UndoableDiagram() {
const instance = useRef(null);
return (
<div style={{ height: 400 }}>
<button onClick={() => { void instance.current?.getEngine().undo(); }}>Undo</button>
<button onClick={() => { void instance.current?.getEngine().redo(); }}>Redo</button>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
onInit={(value: DiagramInstance) => { instance.current = value; }} />
</div>
);
}
vue<script setup lang="ts"> import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue'; const 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' }, ]; const edges = [{ id: 'e1', source: 'a', target: 'b' }]; let instance: DiagramInstance | null = null; function onInit(value: DiagramInstance): void { instance = value; } function undo(): void { void instance?.getEngine().undo(); } function redo(): void { void instance?.getEngine().redo(); } </script> <template> <div style="height:400px"> <button @click="undo">Undo</button> <button @click="redo">Redo</button> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" /> </div> </template>
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.
| Call | Returns | Use |
|---|---|---|
engine.undo() | Promise<void> | Restore the last command. |
engine.redo() | Promise<void> | Reapply the last undone command. |
engine.canUndo() | boolean | Set the Undo button's disabled state. |
engine.canRedo() | boolean | Set 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; useengine.addNode()or another command-backed engine operation. - Do not treat
undo()andredo()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.
See Commands and undo for command composition and Instance and reconciliation for the live instance surface.
Was this page helpful?