Grafloria puts user gestures and user-directed programmatic edits on the same command history, then lets each framework binding return the changed models to your application state.
mermaidflowchart LR G["Gesture"] --> C["Command"] P["Programmatic user edit"] --> C C --> M["CommandManager"] M --> D["DiagramModel"] D --> B["Framework binding"] B --> S["Application state"]
Gestures are already commands
When you mount a canvas, dragging, connecting, deleting, pasting, and grouping use the engine's command path. A drag is committed as one history step, so keyboard undo returns the node to the position where that gesture began rather than undoing individual pointer updates. Redo reapplies the gesture.
The binding owns the canvas, but the history belongs to its engine. In React and Vue, reach it through DiagramInstance and getEngine(). Angular's DiagramCanvasComponent also exposes mirrored undo() and redo() methods. undo() is not a renderer-instance method; see How Grafloria works for the layer boundary.
Put your edit on the same stack
Mount the framework component, capture its instance, and send an edit made on the user's behalf through the engine's command manager. The following React example renders two nodes, adds a third node through AddNodeCommand, and updates the visible node count when the binding receives the model change.
tsximport { useRef, useState, type ReactElement } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import { AddNodeCommand, NodeModel } from '@grafloria/engine';
import type { DiagramInstance, NodeSpec } from '@grafloria/renderer';
const nodes: NodeSpec[] = [
{ id: 'start', position: { x: 80, y: 100 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'finish', position: { x: 360, y: 100 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } },
];
export function CommandHistoryExample(): ReactElement {
const instanceRef = useRef<DiagramInstance | null>(null);
const [nodeCount, setNodeCount] = useState(nodes.length);
const addNode = async (): Promise<void> => {
const instance = instanceRef.current;
if (!instance) return;
const node = new NodeModel({
id: 'review',
type: 'default',
position: { x: 220, y: 240 },
size: { width: 140, height: 60 },
});
await instance.getEngine().commandManager.execute(new AddNodeCommand(node));
};
const undo = async (): Promise<void> => {
const instance = instanceRef.current;
if (instance) await instance.getEngine().commandManager.undo();
};
return (
<div>
<div style={{ display: 'flex', gap: '8px' }}>
<button type="button" onClick={addNode}>Add review node</button>
<button type="button" onClick={undo}>Undo</button>
<span>Nodes: {nodeCount}</span>
</div>
<div style={{ height: '320px', width: '640px' }}>
<GrafloriaFlow
defaultNodes={nodes}
onInit={(instance): void => { instanceRef.current = instance; }}
onNodesChange={(changed): void => { setNodeCount(changed.length); }}
/>
</div>
</div>
);
}
Click Add review node to see a third node and a count of three. Click Undo to remove that node and receive a count of two. The call to commandManager.execute() records the edit; the binding's onNodesChange callback receives the resulting NodeModel[] after both execution and undo.
Keep setup separate from user edits
There are two intents:
- Build, load, import, or synchronize a document with direct model mutations. Those mutations establish the starting document and do not become undo steps.
- Change the document on the user's behalf with
CommandManager.execute()runs the command asynchronously and records an undoable command when it succeeds.
This distinction prevents loading a saved file from filling the user's undo history. It also makes a toolbar action or automated suggestion behave like a gesture: the user can undo it with the same keyboard shortcut and the same history controls.
Group edits into one step
Use BatchCommand when one user action performs several changes. Construct it with a name and the commands it contains, then execute it through the same command manager. One undo calls the batch's undo operation and reverses the whole group, rather than exposing each child as a separate step.
Commands can also refuse execution through canExecute(context). A refused command does not enter history, so there is no misleading later undo for an action that never happened. A command that does execute supplies undo(context) and can supply redo(context); the default redo re-executes the command.
History returns through bindings
Undo changes the live models in the engine. The bindings then report that changed model state through their normal data surfaces: React calls onNodesChange with the reverted NodeModel[]; Angular writes the result through [(nodes)]; Vue updates v-model:nodes. Keep one application state path connected to those binding updates instead of maintaining a second undo stack.
The engine keeps the history on DiagramEngine, whose commandManager also exposes canUndo(), canRedo(), getHistory(), and history-size controls. Use those methods to drive disabled states and history UI; use the mounted instance or framework component to render the result.
Where this fits
Use direct model operations for initial data and synchronization. Use commands for actions the user needs to reverse. Start at the framework binding, use DiagramInstance for the mounted diagram, and reach the engine only for behavior such as history. Continue with the model and documents, the instance and lifecycle, or collaboration.
Was this page helpful?