Skip to content
D
Documentation

Commands, events, and undo

concept
2 min readUpdated

Grafloria records edits through the engine's command path and reports the resulting model change through the instance event map and your framework binding. This keeps the canvas and your application state synchronized without a second undo stack.

One path from edit to state

The DiagramInstance is the handle shared by the framework wrappers. Its model stores nodes and edges; its DiagramEngine owns commands, validation, and history.

mermaid
flowchart LR
  G["DiagramEngine.addNode()"] --> C["AddNodeCommand"]
  C --> H["Engine history"]
  H --> M["Diagram model"]
  M --> E["Instance event map"]
  E --> B["React callbacks, Vue emits, Angular outputs, or Qwik QRLs"]
  B --> S["Application state"]

Listen at the instance

Use instance.on() when you need the shared event map or when the event is not represented by a component callback. The call returns an unsubscribe function; call it when the owner of the listener is disposed.

ts
import type { DiagramInstance } from '@grafloria/renderer';

export function watchDiagram(instance: DiagramInstance) {
  const offNodes = instance.on('nodes:change', ({ nodes }) => {
    console.log('nodes:', nodes.length);
  });
  const offSelection = instance.on('selection:change', ({ nodes, edges }) => {
    console.log('selected:', nodes.length, edges.length);
  });

  return () => {
    offNodes();
    offSelection();
  };
}

nodes:change reports the live node models after nodes are added, removed, or changed. selection:change reports the selected node and edge models. Other instance events include edges:change, connect, reconnect, node:click, node:doubleclick, edge:click, and viewport:change; their payloads describe the completed interaction in diagram coordinates where applicable.

The same history in each framework

The event map stays the same; only the binding syntax changes.

Vue

The Vue GrafloriaFlow wrapper emits the same interactions in Vue form. Bind the controlled arrays with v-model and retain the instance from @init:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance, NodeSpec } from '@grafloria/renderer';

const nodes = ref<NodeSpec[]>([
  { id: 'start', type: 'rect', label: 'Start', position: { x: 40, y: 80 }, size: { width: 140, height: 56 } },
  { id: 'finish', type: 'rect', label: 'Finish', position: { x: 280, y: 80 }, size: { width: 140, height: 56 } },
]);
const instance = ref<DiagramInstance | null>(null);

function onInit(ready: DiagramInstance) {
  instance.value = ready;
}
</script>

<template>
  <div style="height: 320px">
    <GrafloriaFlow v-model:nodes="nodes" @init="onInit" @selection-change="console.log" />
  </div>
  <button type="button" @click="instance?.getEngine().undo()">Undo</button>
  <button type="button" @click="instance?.getEngine().redo()">Redo</button>
</template>
The Vue canvas shows the Start and Finish nodes with Undo and Redo buttons below it.

v-model:nodes receives the reverted array after undo, so the displayed nodes and Vue state describe the same document.

Angular

Angular's DiagramCanvasComponent mirrors undo() and redo() as component methods. In controlled mode, two-way nodes and edges bindings receive model changes:

html
<grafloria-diagram-canvas
  #canvas
  [(nodes)]="nodes"
  [(edges)]="edges"
  (modelChange)="persist($event)"
></grafloria-diagram-canvas>
<button type="button" (click)="canvas.undo()">Undo</button>
<button type="button" (click)="canvas.redo()">Redo</button>

Keep a template reference to the mounted component (#canvas) when using this form. For events that Angular does not expose as outputs, subscribe through the active engine's event bus rather than creating a separate history.

Qwik

Qwik's GrafloriaFlow uses the same instance and data model, with QRL callback props. Use onInit$ to retain the instance and onNodesChange$ for the model callback in a Qwik component. The callback receives the reconciled models after undo or redo.

Choose the edit path

Use the diagram model's direct operations for setup, import, and synchronization: those operations establish the document and do not represent a user's undoable action. Use the engine's command manager for an application-initiated edit that belongs in the user's history, such as a toolbar action or an assistant suggestion. A refused command does not enter history; in strict validation mode, an invalid result is reverted and is not recorded.

Do not call undo() on DiagramInstance; it is not an instance method. Call instance.getEngine().undo() instead. Angular's canvas component is the exception because it mirrors the method directly.

Was this page helpful?