# 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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance) and call `undo()` or `redo()` on its engine. In Angular, [`DiagramCanvasComponent`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-angular-components-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](https://grafloria.com/demos/interaction/drag-undo.html). 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`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-element-core); use `getEngine().addNode()` for a programmatic addition. The typed sample data uses [`NodeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-nodespec) and [`EdgeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-edgespec); Angular controlled arrays can also carry live [`NodeModel`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-models-nodemodel) and [`LinkModel`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-engine-models-linkmodel) instances. For React use [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-react); for Vue use [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-vue); for Qwik use [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-qwik). 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.

:::code-group
```js title="JavaScript"
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(); });
```
```ts title="Angular"
import { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { LinkModel, NodeModel } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  selector: 'app-undo-redo',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div style="height:460px">
      <div>
        <button type="button" (click)="addSuggestion()">Add node</button>
        <button type="button" (click)="undo()">Undo</button>
        <button type="button" (click)="redo()">Redo</button>
      </div>
      <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
        style="display:block;height:420px" />
    </div>
  `,
})
export class UndoRedoComponent {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  nodes: readonly (NodeSpec | NodeModel)[] | undefined = [
    { 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' },
  ];
  edges: readonly (EdgeSpec | LinkModel)[] | undefined = [{ id: 'e1', source: 'a', target: 'b' }];
  private nextSuggestion = 1;

  addSuggestion(): void {
    const number = this.nextSuggestion++;
    const engine = this.canvas().activeEngine();
    if (engine) void engine.addNode({
      type: 'rect',
      position: { x: 220 + (number - 1) * 24, y: 300 },
      size: { width: 160, height: 70 },
    });
  }

  undo(): void { void this.canvas().undo(); }
  redo(): void { void this.canvas().redo(); }
}
```
```tsx title="Qwik"
import { component$ } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { 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' },
];
const edges: EdgeSpec[] = [];

export default component$(() => (
  <div style={{ height: '460px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
  </div>
));
```
```vue title="Vue"
<script setup lang="ts">
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { 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' },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];
let instance: DiagramInstance | null = null;
let nextSuggestion = 1;

function onInit(diagramInstance: DiagramInstance): void { instance = diagramInstance; }
function addSuggestion(): void {
  const number = nextSuggestion++;
  const engine = instance?.getEngine();
  if (engine) void engine.addNode({
    type: 'rect',
    position: { x: 220 + (number - 1) * 24, y: 300 },
    size: { width: 160, height: 70 },
  });
}
function undo(): void { const engine = instance?.getEngine(); if (engine) void engine.undo(); }
function redo(): void { const engine = instance?.getEngine(); if (engine) void engine.redo(); }
</script>

<template>
  <div style="height:460px">
    <div>
      <button type="button" @click="addSuggestion">Add node</button>
      <button type="button" @click="undo">Undo</button>
      <button type="button" @click="redo">Redo</button>
    </div>
    <div style="height:420px">
      <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" />
    </div>
  </div>
</template>
```
```tsx title="React"
import { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { 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' },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b' }];

export default function UndoRedoExample() {
  const instance = useRef<DiagramInstance | null>(null);
  const nextSuggestion = useRef(1);

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

  return (
    <div style={{ height: '460px' }}>
      <div>
        <button type="button" onClick={addSuggestion}>Add node</button>
        <button type="button" onClick={() => { const engine = instance.current?.getEngine(); if (engine) void engine.undo(); }}>Undo</button>
        <button type="button" onClick={() => { const engine = instance.current?.getEngine(); if (engine) void engine.redo(); }}>Redo</button>
      </div>
      <div style={{ height: '420px' }}>
        <GrafloriaFlow
          defaultNodes={nodes}
          defaultEdges={edges}
          onInit={(diagramInstance) => { instance.current = diagramInstance; }}
        />
      </div>
    </div>
  );
}
```
:::

## 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](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/how-grafloria-works) for the instance, engine, and model roles.

| Input | Type | Default | Effect |
| --- | --- | --- | --- |
| `defaultNodes` | [`NodeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-nodespec) `[]` | Not stated | Initial nodes for React and Vue. |
| `defaultEdges` | [`EdgeSpec`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-edgespec) `[]` | Not stated | Initial edges for React and Vue. |
| `nodes` | `readonly (NodeSpec \| NodeModel)[] \| undefined` | `undefined` | Angular's controlled node data; two-way binding reflects canvas edits. |
| `edges` | `readonly (EdgeSpec \| LinkModel)[] \| undefined` | `undefined` | Angular'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.

## Related

- [Command history and edits](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) explains commands and the shared history in more depth.
- [Synchronize diagrams](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/synchronize-diagrams) covers collaboration between diagrams.
- [React quick start](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/react-quick-start), [Vue quick start](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/vue-quick-start), [Angular quick start](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/angular-quick-start), and [Qwik quick start](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/qwik-quick-start) show the surrounding framework setup.
- [Collaboration-aware undo demo](https://grafloria.com/demos/interaction/undo-redo.html) shows how a peer's undo applies to that peer's own edits.
