Grafloria is a diagram engine whose framework bindings share one headless model, so your choice of framework changes how you bind data—not what the diagram means.
The five ideas below explain where to put data, edits, persistence and drawing code.
mermaidflowchart LR A["Application specs"] --> B["Binding / DiagramInstance"] B --> C["Live model: document data"] D["Engine: commands and history"] --> C C --> E["Renderer: geometry and pixels"] C --> F["Shared serialized document"] C --> B B --> A
1. Specs describe intent; live models hold data
Framework bindings convert plain specs into live models; the examples below trace those specs through reconciliation, command-backed edits and serialization—see the introduction for the API entry points.
This browser example uses render to mount two connected nodes. Its return value is the same instance facade the bindings expose. Install the packages in your own browser project:
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine
Use RenderSpec to check the input. graph.ts declares the data and a mounting function; your browser entry point calls it with a sized container.
tsimport { render, type RenderSpec } from '@grafloria/element';
export const spec = {
nodes: [
{ id: 'intake', label: 'Intake', position: { x: 60, y: 80 },
size: { width: 120, height: 48 } },
{ id: 'review', label: 'Review', position: { x: 280, y: 80 },
size: { width: 120, height: 48 } },
],
edges: [{ id: 'next', source: 'intake', target: 'review' }],
} satisfies RenderSpec;
export function mountGraph(host: HTMLElement) {
return render(spec, host);
}
tsimport { mountGraph } from './graph';
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);
const instance = mountGraph(host);
console.log(instance.getModel().getNode('intake'));
You see Intake connected to Review. The query returns the live node, not the input spec. For the full distinction, read Specs and live models; for mounting and cleanup, read Instance and lifecycle.
The remaining browser entry files call the same mounting function. Run each separately alongside graph.ts.
2. Pick a state owner
Uncontrolled defaults seed the instance once; the instance owns subsequent edits. Controlled inputs make your application the state owner and require a return path for changes. Reconciliation updates existing spec-backed models by id rather than remounting the diagram, preserving live identity and leaving selection alone when you omit selected.
This sample changes Review's position through the instance's spec surface. The node moves down, and the identity assertion checks that the existing live object remains in use.
tsimport { mountGraph, spec } from './graph';
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);
const instance = mountGraph(host);
const before = instance.getModel().getNode('review');
instance.setNodes(spec.nodes.map(node =>
node.id === 'review'
? { ...node, position: { x: 280, y: 180 } }
: node
));
instance.renderNow();
console.assert(before === instance.getModel().getNode('review'));
In a controlled component, connect both directions using the binding's own idiom:
| Binding | Input and change return path |
|---|---|
React GrafloriaFlow | nodes / edges with onNodesChange / onEdgesChange; the state hooks convert live models back to specs. |
Vue GrafloriaFlow | v-model:nodes / v-model:edges write changes back to your refs. |
Qwik GrafloriaFlow | nodes / edges with onNodesChange$ / onEdgesChange$ return spec arrays. |
Angular DiagramCanvasComponent | [(nodes)] / [(edges)] round-trip through model signals. |
See State and event flow for this loop, and the React quick start for the controlled-state wiring.
React
Render the same two-node graph with uncontrolled defaults; the canvas owns subsequent edits. Type the arrays with NodeSpec and EdgeSpec.
bashnpm install @grafloria/react @grafloria/engine @grafloria/renderer @grafloria/element react react-dom
tsximport { GrafloriaFlow } from '@grafloria/react';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { spec } from './graph';
const nodes: NodeSpec[] = spec.nodes;
const edges: EdgeSpec[] = spec.edges;
export default function Editor() {
return <div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
</div>;
}
Vue
The Vue component seeds the same graph with plain specs.
bashnpm install @grafloria/vue @grafloria/engine @grafloria/renderer @grafloria/element vue
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; import type { NodeSpec, EdgeSpec } from '@grafloria/renderer'; import { spec } from './graph'; const nodes: NodeSpec[] = spec.nodes; const edges: EdgeSpec[] = spec.edges; </script> <template> <div style="height: 400px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" /> </div> </template>
Qwik
The Qwik component passes serializable spec data, not a live instance.
bashnpm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
tsximport { component$ } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { spec } from './graph';
const nodes: NodeSpec[] = spec.nodes;
const edges: EdgeSpec[] = spec.edges;
export default component$(() => (
<div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
</div>
));
Angular
Angular's two-way bindings keep application arrays in sync with edits to the same graph. Type component data with the library's NodeSpec and EdgeSpec types.
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer @grafloria/element rxjs
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { spec } from './graph';
@Component({
selector: 'app-editor',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block; height:400px" />
`,
})
export class EditorComponent {
nodes: readonly NodeSpec[] = spec.nodes;
edges: readonly EdgeSpec[] = spec.edges;
}
3. Loading and editing are different intents
Setup writes directly to the model; user-facing edits become commands on the engine's history stack. Built-in gestures follow that second path: one drag is one undo step, not one step per position update.
Use engine methods that execute commands for your toolbar actions. Here, clicking Add task adds a labelled node and returns its live model. The engine's addNode() implementation constructs and executes a shipped add-node command, so the edit joins the same history as gestures.
tsimport { mountGraph } from './graph';
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);
const instance = mountGraph(host);
const button = document.createElement('button');
button.textContent = 'Add task';
document.body.prepend(button);
let nextY = 180;
button.onclick = async () => {
const y = nextY;
nextY += 70;
const node = await instance.getEngine().addNode({
type: 'task',
position: { x: 60, y },
size: { width: 120, height: 48 },
data: { label: 'New task' },
});
instance.renderNow();
console.log(node);
};
For domain actions that need several mutations, execute command objects through commandManager.execute() rather than treating model writes as edits. Controlled bindings feed the reverted models back to application state after undo. Read Commands and history for command composition and history controls.
4. The document is the API
Save the live model in the shared serialization format, not a framework's projection of it. The versioned document carries nodes—including their ports—links, groups and viewport. It is the common representation for persistence and collaboration.
This sample adds a Save document button. Click it after editing to see JSON from the current live model in the page, including its schemaVersion.
tsimport { mountGraph } from './graph';
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);
const instance = mountGraph(host);
const button = document.createElement('button');
button.textContent = 'Save document';
const output = document.createElement('pre');
document.body.prepend(button);
document.body.append(output);
button.onclick = () => {
const document = instance.getModel().serialize();
const camera = instance.viewport.getState();
output.textContent = JSON.stringify({ document, camera }, null, 2);
};
Known issue: Serializing the model alone does not save the mounted canvas's current pan and zoom: camera changes do not update
DiagramModel.viewport, and mounting a restored document does not apply its saved camera. Until it is fixed, saveinstance.viewport.getState()separately, as above, and after mounting restore it withinstance.viewport.setViewport(camera.viewport)andinstance.viewport.setZoom(camera.zoom).
Kits follow the same model: ordinary nodes and edges plus a wiring step that attaches behavior to the mounted instance. Loading a document restores saved structure and reattaches built-in kit behavior; your application's custom painters still belong to your application. See Documents and kits for kit mounting, and Save and restore documents for restoration.
5. Geometry is intent, not pixels
Declare what connects and how it routes; let the engine and renderer compute geometry as nodes move. In EdgeSpec, router says where the line goes, connector says how it is drawn, and waypoints constrain its bends. Endpoints can name nodes without pinning specific ports.
Groups also express intent: GroupSpec names real children, not merely a rectangle behind them. This sample replaces the connection with an orthogonal route and puts both nodes in one fitted group. You see a group around Intake and Review with a right-angled connection between them.
tsimport { mountGraph, spec } from './graph';
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(host);
const instance = mountGraph(host);
instance.setNodes(spec.nodes.map(node =>
node.id === 'review'
? { ...node, position: { x: 280, y: 180 } }
: node
));
instance.setEdges([{
id: 'next', source: 'intake', target: 'review',
type: 'orthogonal',
}]);
instance.setGroups([{
id: 'order', label: 'Order flow',
children: ['intake', 'review'], padding: 30,
}]);
instance.renderNow();
For custom nodes, your component or renderer paints the inside of an engine-positioned HTML host. It does not take over dragging, selection, ports or connections. Continue with Route and label edges, Group and nest nodes, or JavaScript: elements and content.
See it running
Try the live interaction demos: drag a node, then undo the gesture. The demo source shows the same engine underneath the bindings.
Was this page helpful?