Skip to content
D
Documentation

Instance and bindings

concept
3 min readUpdated

Grafloria has one headless engine behind its JavaScript, React, Vue, Angular, Qwik, and custom-element surfaces. Choose the surface that matches your host, then use the shared DiagramInstance when you need the live diagram.

Choose the layer for the job

The model owns diagram data. The engine owns behavior. The binding connects those layers to your framework's lifecycle and state.

mermaid
flowchart TB
  UI["React / Vue / Angular / Qwik / HTML"] --> B["Framework binding"]
  B --> I["DiagramInstance"]
  I --> M["DiagramModel: nodes, links, groups, viewport"]
  I --> E["DiagramEngine: commands, layout, validation, undo"]
  M --> R["Renderer and layout: geometry and pixels"]
  E --> R
NeedUseWhy
Supply or replace nodes and edgesThe binding's data props, or setNodes() / setEdges() on the instanceSpecs are reconciled into live models. Persistent IDs keep existing live models.
Query nodes, links, groups, or selectioninstance.getModel()The model is the document's live source of truth.
Layout, validation, commands, or undoinstance.getEngine()Behavior belongs to the engine, not the instance's data API.
Paint after a mutationrender() or renderNow()render() queues a coalesced repaint; renderNow() paints synchronously when you must measure immediately.
Listen for changesinstance.on() or the binding's event propsEvery binding exposes the same event map in its native idiom.
Read or change the camerainstance.viewport and fitView()Viewport data is separate from node data.
Read commentsgetCommentStore()It returns the enabled comment store, or null when comments are disabled.

Do not put engine behavior into framework state. Keep application state for data that the rest of your application owns, such as a saved document or an inspector selection. Keep the live diagram in the instance and subscribe to the events or hooks that your UI needs.

The same instance from each surface

For plain JavaScript, render mounts a data spec and returns the instance immediately. Its target can be an element or a CSS selector; the spec is data, not Mermaid text.

js
import { render } from '@grafloria/element';

const host = document.createElement('div');
host.id = 'diagram';
host.style.height = '240px';
document.body.append(host);

const api = render(
  {
    nodes: [
      { id: 'plan', position: { x: 40, y: 40 }, size: { width: 140, height: 56 }, label: 'Plan' },
      { id: 'ship', position: { x: 280, y: 40 }, size: { width: 140, height: 56 }, label: 'Ship' },
    ],
    edges: [{ id: 'plan-ship', source: 'plan', target: 'ship' }],
  },
  host,
  { fitView: true },
);

const stopListening = api.on('selection:change', ({ nodes }) => {
  console.log(`Selected ${nodes.length} node(s)`);
});

api.fitView(24);

The mounted host shows two boxes and a connecting edge. Selecting a box invokes the handler with the live selected models. fitView() frames both boxes; call dispose() from your host's teardown path rather than immediately after mounting in an application that still displays the diagram.

In React, GrafloriaFlow is the component surface. Its onInit callback supplies the same instance that render() returns. Use GrafloriaProvider when a toolbar or inspector is outside the flow subtree, then read the handle with useGrafloria. The subscription hooks expose framework state without copying the whole model into React.

tsx
import {
  GrafloriaFlow,
  GrafloriaProvider,
  useGrafloria,
  useOnSelectionChange,
  useSelection,
  useViewport,
  type DiagramInstance,
  type EdgeSpec,
  type NodeSpec,
} from '@grafloria/react';
import { useState } from 'react';

const nodes: NodeSpec[] = [
  { id: 'plan', position: { x: 40, y: 40 }, size: { width: 140, height: 56 }, label: 'Plan' },
  { id: 'ship', position: { x: 280, y: 40 }, size: { width: 140, height: 56 }, label: 'Ship' },
];
const edges: EdgeSpec[] = [{ id: 'plan-ship', source: 'plan', target: 'ship' }];

function Toolbar() {
  const instance = useGrafloria();
  const selection = useSelection();
  const viewport = useViewport();
  const [lastSelection, setLastSelection] = useState(0);
  useOnSelectionChange(({ nodes: changedNodes }) => setLastSelection(changedNodes.length));
  return (
    <div>
      <button type="button" onClick={() => instance?.fitView(24)}>Fit view</button>
      <span> zoom {viewport.zoom.toFixed(2)}</span>
      <span> selected {selection.nodes.length} node(s)</span>
      <span> last change {lastSelection}</span>
    </div>
  );
}

export default function DiagramPage() {
  const [instance, setInstance] = useState<DiagramInstance | null>(null);
  return (
    <GrafloriaProvider>
      <Toolbar />
      <div style={{ height: 400 }}>
        <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} fitView onInit={setInstance} />
      </div>
      <p>{instance ? 'Diagram ready' : 'Starting diagram'}</p>
    </GrafloriaProvider>
  );
}

The canvas renders inside the sized div; the toolbar reports the live zoom and selection, and the paragraph changes when the instance mounts. defaultNodes and defaultEdges seed an uncontrolled canvas. If application state must own the graph, pass nodes and edges together with onNodesChange and onEdgesChange instead. Do not mix controlled nodes with uncontrolled defaults: a stale controlled array can overwrite a user edit on the next render.

Vue exposes the same component-level idea through GrafloriaFlow props and emits, including @selection-change and @connect. Angular exposes controlled [(nodes)] / [(edges)] bindings and outputs such as (viewportChanged) and (layoutDone). The custom element forwards the same instance events as bubbling CustomEvents. The underlying model and engine do not change when the binding changes.

Data, behavior, and rendering are different calls

Start with the instance, then descend only for the responsibility you need:

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

async function inspect(api: DiagramInstance): Promise<void> {
  const model = api.getModel();
  const selected = model.getSelectedNodes();
  const engine = api.getEngine();
  const validation = engine.validateDiagram();
  await engine.layout('elk');
  api.renderNow();
  console.log(selected, validation);
}

The first two calls query live data and behavior respectively; the layout call changes geometry through the engine; renderNow() makes the resulting pixels current before an immediate measurement. Undo is also an engine operation: call api.getEngine().undo(), not api.undo().

For persistence, serialize the model rather than a framework's stale mirror. For text round-trips, use exportText() and loadText() on the instance. For a screenshot or document export, use the instance's export methods. These operations act on the mounted diagram, so they include edits made through gestures.

Events and framework state

The instance event map is the common language:

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

function watch(api: DiagramInstance): () => void {
  const stopNode = api.on('node:click', ({ node, world }) => {
    console.log(node.id, world.x, world.y);
  });
  const stopViewport = api.on('viewport:change', ({ viewport, zoom }) => {
    console.log(viewport, zoom);
  });
  return () => {
    stopNode();
    stopViewport();
  };
}

on() returns an unsubscribe function. React callback props, Vue emits, Angular outputs, and element DOM events dress the same map in framework-native forms. Use the component event first when it exists; reach through on() when you need an instance event that the component does not expose.

For React UI, useSelection is state for rendering an inspector, useViewport is state for a zoom badge or minimap, and useOnSelectionChange is a callback for side effects. They subscribe to the live instance; they are not a second diagram model.

A practical rule

Pass specs through the binding when the framework owns the document. Use the instance for the mounted diagram's events, camera, painting, export, and text conversion. Use getModel() for queries and getEngine() for commands, layout, validation, and undo. This keeps one document, one history, and one event map across every binding.

Was this page helpful?