Skip to content
D
Documentation

The instance and data flow

concept
3 min readUpdated

The live DiagramInstance connects your diagram specs to the rendered canvas: it reconciles application data into live models, coordinates model changes with the renderer, and exposes the engine for diagram behavior.

One model, three responsibilities

The framework bindings are thin wrappers around one headless model. They accept NodeSpec and EdgeSpec data, create a live instance, and reflect model changes through their framework's data contract. The instance is the facade between the canvas, the data model, and the engine:

mermaid
flowchart LR
  A["Application specs"] -->|"props or setNodes() / setEdges()"| I["DiagramInstance"]
  I -->|reconcile| M["DiagramModel: live nodes, links, groups"]
  M -->|invalidations and scheduled paint| R["Renderer: SVG and HTML layers"]
  M -->|model events| I
  I -->|callbacks, events, or two-way updates| A
  I -->|"getEngine()"| E["DiagramEngine: behavior"]

Think of the layers this way: specs are convenient application input; the live model is the diagram's data; the engine owns behavior such as commands and validation; and the renderer turns model intent into pixels. The instance joins those layers so the same model drives each framework binding.

Mounting hands you the live instance

In browser JavaScript, render mounts a data spec and returns the instance for that canvas. This runnable example starts with one node. Clicking Add node reconciles a second node into the mounted diagram; the instance event listener reports the live model's node count.

ts
import { render } from '@grafloria/element';
import type { NodeSpec } from '@grafloria/renderer';

const host = document.createElement('div');
host.style.cssText = 'width: 100%; height: 400px';
document.body.append(host);

const button = document.createElement('button');
button.textContent = 'Add node';
document.body.prepend(button);

const specs: NodeSpec[] = [
  { id: 'first', position: { x: 40, y: 60 }, label: 'First' },
];
let currentSpecs = specs;
const instance = render({ nodes: specs, edges: [] }, host);

instance.on('nodes:change', ({ nodes }) => {
  console.log(`Live nodes: ${nodes.length}`);
});

button.addEventListener('click', () => {
  const next: NodeSpec = {
    id: `node-${currentSpecs.length + 1}`,
    position: { x: 220, y: 60 },
    label: 'Second',
  };
  currentSpecs = [...currentSpecs, next];
  instance.setNodes(currentSpecs);
});

The canvas displays both labeled nodes after the click. setNodes() returns void; the event listener receives the updated live node models when the model's membership changes. Give the host a resolved height so the canvas has space to draw.

Use the same flow through a framework

The wrappers mount that same model through their own component conventions. Their instance callback gives you the same DiagramInstance, while controlled inputs let application state own the specs. In the React, Vue, and Angular examples below, the Add node button shows the inbound update path.

React

GrafloriaFlow accepts defaultNodes for an instance-owned graph. onInit captures the instance, and onNodesChange receives live node models when membership changes.

tsx
import { useRef } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance, NodeSpec } from '@grafloria/renderer';

const initialNodes: NodeSpec[] = [
  { id: 'first', position: { x: 40, y: 60 }, label: 'First' },
];

export function ReactFlowExample() {
  const instance = useRef<DiagramInstance | null>(null);
  const specs = useRef<NodeSpec[]>(initialNodes);

  function addNode() {
    const next: NodeSpec = {
      id: `node-${specs.current.length + 1}`,
      position: { x: 220, y: 60 },
      label: 'Second',
    };
    specs.current = [...specs.current, next];
    instance.current?.setNodes(specs.current);
  }

  return (
    <>
      <button onClick={addNode}>Add node</button>
      <div style={{ height: 400 }}>
        <GrafloriaFlow
          defaultNodes={initialNodes}
          onInit={(live) => { instance.current = live; }}
          onNodesChange={(nodes) => console.log(`Live nodes: ${nodes.length}`)}
        />
      </div>
    </>
  );
}

The click adds a node to the live model, and onNodesChange observes the membership update. In controlled React mode, pass nodes with onNodesChange; otherwise a later render can reconcile stale application state back into the canvas. See the React quick start for the controlled ownership pattern.

Vue

GrafloriaFlow emits init with the instance. This example uses the instance to update its live model; Vue's component does not declare an onNodesChange callback, so the code subscribes to the instance's nodes:change event.

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

const initialNodes: NodeSpec[] = [
  { id: 'first', position: { x: 40, y: 60 }, label: 'First' },
];
const specs = ref<NodeSpec[]>(initialNodes);
const instance = shallowRef<DiagramInstance | null>(null);

function onInit(live: DiagramInstance) {
  instance.value = live;
  live.on('nodes:change', ({ nodes }) => {
    console.log(`Live nodes: ${nodes.length}`);
  });
}

function addNode() {
  const next: NodeSpec = {
      id: 'second',
    position: { x: 220, y: 60 },
    label: 'Second',
  };
  specs.value = [...specs.value, next];
  instance.value?.setNodes(specs.value);
}
</script>

<template>
  <button @click="addNode">Add node</button>
  <div style="height: 400px">
    <GrafloriaFlow :default-nodes="initialNodes" @init="onInit" />
  </div>
</template>

The click adds the second node, and the nodes:change listener reports the live model's node count. For controlled Vue data, use v-model:nodes; the wrapper reconciles that prop into the model and emits updated specs when the controlled nodes change. See the Vue quick start.

Angular

Angular's DiagramCanvasComponent follows the same model flow through signal-based two-way bindings. Here the component instance remains behind its Angular component boundary: the host owns a typed spec array, and the binding reconciles it into the rendered diagram.

ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

@Component({
  selector: 'app-flow-example',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <button type="button" (click)="addNode()">Add node</button>
    <grafloria-diagram-canvas
      [(nodes)]="nodes"
      [(edges)]="edges"
      style="display:block; height:400px" />
  `,
})
export class FlowExampleComponent {
  nodes: NodeSpec[] = [
    { id: 'first', position: { x: 40, y: 60 }, label: 'First' },
  ];
  edges: EdgeSpec[] = [];

  addNode(): void {
    this.nodes = [
      ...this.nodes,
      { id: `node-${this.nodes.length + 1}`, position: { x: 220, y: 60 }, label: 'Second' },
    ];
  }
}

Clicking Add node updates the host array; [(nodes)] feeds it into the live model and writes canvas-side node changes back to the host. Angular also exposes modelChange for incremental model patches. See the Angular quick start.

Reconciliation preserves the live diagram

setNodes() and setEdges() reconcile incoming specs rather than rebuilding the diagram. New ids add models, missing ids remove them, and ids that remain update their existing live objects. The renderer, selection, listeners, and mounted plugins stay attached to the same instance; the next scheduled paint reflects the model change.

That identity preservation matters when your app passes a changed array for a graph whose ids remain stable. It also means a same-id spec updates the live model rather than replacing the entire document. When importing a different document that reuses ids and must replace all current state, clear the existing specs before applying the new ones. The reconciliation details and import pattern are covered in Add nodes from palettes.

Model change events are not a frame-by-frame drag feed. Structural changes such as adding or removing nodes emit nodes:change; moving or editing a node schedules a repaint, while changes from an interaction are reflected at its commit points. The wrappers project data according to their contracts: React's callback receives live models, while Vue's controlled output provides specs. Treat those values as application synchronization, not as a full-document persistence format.

Choose the layer for the task

Start with the instance for mounted-canvas work: getModel() reads live diagram data, getEngine() reaches engine behavior, and setNodes() / setEdges() reconcile input. Use the framework component's controlled binding when application state owns the graph. The instance itself does not provide undo; history lives on the engine. The DiagramInstance reference maps the full surface, while Command history explains the engine's undoable edits.

See the live React save-and-restore demo, Vue save-and-restore demo, and Angular snapshot-and-restore demo to watch model data travel through a mounted canvas.

Was this page helpful?