Skip to content
D
Documentation

Undo and redo edits

how-to
3 min readUpdated

Undo and redo diagram edits

Use the diagram's command history when users need to reverse a gesture or when your feature edits the graph on their behalf. A drag is one undoable step; your own toolbar action can use that same history.

Add undo and redo controls

The canvas already handles Ctrl+Z / ⌘Z for undo and Ctrl+Y / ⌘⇧Z for redo. To add buttons, keep the live DiagramInstance and call undo() or redo() on its engine. In Angular, DiagramCanvasComponent exposes those operations directly.

Each sample starts with two nodes. Drag either node and undo to return it to the start of the drag; redo reapplies the move. In JavaScript, Angular, Vue, and React, Add node adds a rectangle through the engine; Undo removes it and Redo restores it. The Qwik sample demonstrates drag undo with the built-in keyboard shortcuts rather than a custom Add node control.

Live drag-and-undo demo. Its canvas starts with two nodes; drag one before trying undo.

JavaScript, Angular, Qwik, Vue, and React

Each example is a mounted canvas with a real height. In JavaScript, mount with render; use getEngine().addNode() for a programmatic addition. The typed sample data uses NodeSpec and EdgeSpec; Angular controlled arrays can also carry live NodeModel and LinkModel instances. For React use GrafloriaFlow; for Vue use GrafloriaFlow; for Qwik use GrafloriaFlow. React and Vue acquire the instance through their component callback. Angular obtains the canvas with viewChild.required() and calls activeEngine().addNode() for programmatic edits, while its undo and redo buttons call the canvas methods directly. The Qwik sample uses the default-data component and the built-in keyboard shortcuts.

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

/** @type {import('@grafloria/renderer').NodeSpec[]} */
const nodes = [
  { id: 'a', position: { x: 80, y: 80 }, size: { width: 160, height: 70 }, label: 'Drag, then undo' },
  { id: 'b', position: { x: 360, y: 180 }, size: { width: 160, height: 70 }, label: 'Every step counts' },
];
/** @type {import('@grafloria/renderer').EdgeSpec[]} */
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

const wrapper = document.createElement('div');
wrapper.style.height = '460px';
document.body.append(wrapper);
const toolbar = document.createElement('div');
wrapper.append(toolbar);
const host = document.createElement('div');
host.style.height = '420px';
wrapper.append(host);
const instance = render({ nodes, edges }, host);

let nextSuggestion = 1;
async function addSuggestion() {
  const number = nextSuggestion++;
  await instance.getEngine().addNode({
    type: 'rect',
    position: { x: 220 + (number - 1) * 24, y: 300 },
    size: { width: 160, height: 70 },
  });
}

/** @param {string} label @param {() => void} action */
function addButton(label, action) {
  const button = document.createElement('button');
  button.textContent = label;
  button.addEventListener('click', action);
  toolbar.append(button);
}

addButton('Add node', () => { void addSuggestion(); });
addButton('Undo', () => { void instance.getEngine().undo(); });
addButton('Redo', () => { void instance.getEngine().redo(); });

What the calls change

undo() and redo() return promises because a command can do asynchronous work. Await them when the next step depends on the history operation finishing. After an undo, the engine's model contains the reverted state. Controlled React, Angular, and Vue bindings reflect model changes back to application data; these React and Vue examples use initial defaultNodes and defaultEdges instead.

For programmatic edits, use command-backed operations when users must be able to undo them; setup and restoration stay outside history. See How Grafloria works for the instance, engine, and model roles.

InputTypeDefaultEffect
defaultNodesNodeSpec []Not statedInitial nodes for React and Vue.
defaultEdgesEdgeSpec []Not statedInitial edges for React and Vue.
nodesreadonly (NodeSpec | NodeModel)[] | undefinedundefinedAngular's controlled node data; two-way binding reflects canvas edits.
edgesreadonly (EdgeSpec | LinkModel)[] | undefinedundefinedAngular's controlled edge data; two-way binding reflects canvas edits.

Pitfalls

  • Do not use model-level mutations for a feature action that users need to undo. Model operations used to set up, load, or synchronize a diagram stay out of the history.
  • Await engine operations before starting a dependent operation. The command methods are asynchronous.
  • A drag is one history step, not one step per pointer movement. Undo returns the node to the position where that drag started.

Was this page helpful?