Grafloria puts one headless engine underneath its JavaScript, React, Vue, Angular, and Qwik surfaces. The engine owns behavior—commands, history, layout, validation, and collaboration—while the model owns the diagram data.
mermaidflowchart TD B["Framework binding"] --> I["DiagramInstance"] I --> M["DiagramModel\nnodes, links, groups, viewport"] I --> E["DiagramEngine\ncommands, layout, validation, history"] M --> R["Renderer\npositions, routes, SVG"] E --> R
Start at the binding
Use the binding's canvas component when your application already uses a framework. The component
mounts a real diagram; its nodes and edges describe the initial or controlled graph, and its
instance callback gives you the shared handle.
React
tsximport { GrafloriaFlow } from '@grafloria/react';
const nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
export default function App() {
return (
<div style={{ height: '100vh' }}>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
plugins
onInit={(instance) => instance.fitView()}
/>
</div>
);
}
The mounted canvas shows two connected nodes. plugins adds the minimap, zoom and fit controls,
and background grid. The onInit callback receives the live instance after mounting.
Vue
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; const nodes = [ { id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } }, { id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } }, ]; const edges = [{ id: 'e1', source: 'a', target: 'b' }]; </script> <template> <div style="height: 100vh"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :plugins="true" /> </div> </template>
Vue renders the same connected diagram. Use v-model:nodes and v-model:edges when the Vue
application owns the live arrays; use the default-* props when the canvas owns them after mount.
Angular
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
selector: 'app-root',
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[plugins]="true" style="display:block; height:100vh" />
`,
})
export class AppComponent {
nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
edges = [{ id: 'e1', source: 'a', target: 'b' }];
}
DiagramCanvasComponent
uses Angular's two-way nodes and edges binding here. A drag changes the live model and writes
the next arrays back through those bindings.
Qwik
tsximport { component$, $ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
const nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
export default component$(() => (
<div style={{ height: '100vh' }}>
<GrafloriaFlow
defaultNodes={nodes}
defaultEdges={edges}
onInit$={$((instance: DiagramInstance) => instance.fitView())}
/>
</div>
));
Qwik uses a QRL callback, but the callback receives the same instance and the canvas shows the same graph. The binding is a thin surface over the shared engine rather than a separate graph engine.
The model is the document
The model is the single source of truth. A DiagramModel
contains nodes, links, groups, and viewport data. A node is a NodeModel;
an input edge uses EdgeSpec. A
GroupSpec describes a zone around child nodes.
Bindings turn plain specs into live models and reconcile later changes by id. Existing ids keep their live objects, new ids create objects, and missing ids are removed. This lets selection, listeners, and renderer state survive ordinary updates. The same document can be persisted, snapshotted, exported, or shared.
For data queries, use instance.getModel(). For example, the model gives you getNodes(),
getLinks(), and getGroups() without reaching into the renderer.
The instance is the shared handle
Plain JavaScript uses render to mount a data spec and return
the live DiagramInstance:
tsimport { render } from '@grafloria/element';
const host = document.getElementById('canvas');
if (!host) throw new Error('Missing #canvas');
host.style.height = '400px';
const instance = render({
nodes: [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
],
edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, host);
instance.fitView();
instance.on('selection:change', ({ nodes }) => console.log(nodes));
Use the instance for specs, painting, events, camera, export, and text round-trips.
setNodes() and setEdges() reconcile data; render() queues a coalesced repaint and
renderNow() repaints synchronously; fitView() frames all content. Dispose the instance from
your application's unmount or close handler, not immediately after mounting.
Geometry comes from layout and routing
Nodes, ports, links, groups, directions, and constraints express semantic intent. Rendering and
layout determine positions and paths. Use the DiagramEngine
for behavior below the instance:
tsimport type { DiagramInstance } from '@grafloria/renderer';
async function arrange(instance: DiagramInstance): Promise<void> {
await instance.getEngine().layout('elk');
instance.renderNow();
}
This lays out the already-mounted diagram and then paints the resulting geometry. Changing node
data does not re-run a declarative layout prop; call the engine's layout method explicitly and
repaint when you drive the instance yourself.
Commands, history, and events
User gestures become commands on one history. Dragging, connecting, deleting, and grouping therefore share undo and redo without application wiring. In React, Vue, and Qwik, reach the engine through the instance; Angular's canvas also exposes its corresponding component methods.
tsimport type { DiagramInstance } from '@grafloria/renderer';
async function undoAndRedo(instance: DiagramInstance): Promise<void> {
await instance.getEngine().undo();
await instance.getEngine().redo();
}
The instance event map is the common event surface. on() returns an unsubscribe function, and
the framework bindings surface the same changes as React callbacks, Vue emits, Angular outputs,
or Qwik QRL callbacks. Use the component event first when the binding provides it; use
instance.on() for events that belong to the shared instance.
Appearance and extension points
The renderer ships LIGHT_THEME and
DARK_THEME. Pass a theme to the component
or call instance.setTheme() to change the mounted diagram's appearance. The renderer also ships
registerTool for canvas tools; it returns a
disposer that restores the previous tool with the same id.
Use the shipped layout adapters, themes, commands, and validators before writing replacements. The live demo gallery shows the same mounted surfaces in operation.
Next steps
- Model and document for serialization and persistence.
- Instance and bindings for controlled state.
- Layout and routing for geometry.
- Commands, events, and undo for history and event details.
Was this page helpful?