# How Grafloria works

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.

```mermaid
flowchart 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/introduction) for the API entry points.

This browser example uses [`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core) to mount two connected nodes. Its return value is the same instance facade the bindings expose. Install the packages in your own browser project:

```bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
```

Use [`RenderSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#renderspec) to check the input. `graph.ts` declares the data and a mounting function; your browser entry point calls it with a sized container.

```ts title="graph.ts"
import { 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);
}
```

```ts title="main.ts"
import { 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/specs-and-live-models); for mounting and cleanup, read [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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.

```ts title="reconcile.ts"
import { 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react) | `nodes` / `edges` with `onNodesChange` / `onEdgesChange`; the state hooks convert live models back to specs. |
| Vue [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue) | `v-model:nodes` / `v-model:edges` write changes back to your refs. |
| Qwik [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik) | `nodes` / `edges` with `onNodesChange$` / `onEdgesChange$` return spec arrays. |
| Angular [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent) | `[(nodes)]` / `[(edges)]` round-trip through model signals. |

See [State and event flow](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/state-and-event-flow) for this loop, and the [React quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec).

```bash
npm install @grafloria/react @grafloria/engine @grafloria/renderer @grafloria/element react react-dom
```

```tsx title="Editor.tsx"
import { 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.

```bash
npm install @grafloria/vue @grafloria/engine @grafloria/renderer @grafloria/element vue
```

```vue title="Editor.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.

```bash
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
```

```tsx title="Editor.tsx"
import { 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.

```bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer @grafloria/element rxjs
```

```ts title="editor.component.ts"
import { 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.

```ts title="edit.ts"
import { 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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`.

```ts title="save.ts"
import { 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, save `instance.viewport.getState()` separately, as above, and after mounting restore it with `instance.viewport.setViewport(camera.viewport)` and `instance.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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/documents-and-kits) for kit mounting, and [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance#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.

```ts title="geometry.ts"
import { 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/route-and-label-edges), [Group and nest nodes](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/group-and-nest-nodes), or [JavaScript: elements and content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content).

## See it running

Try the [live interaction demos](https://grafloria.com/demos/#interaction): drag a node, then undo the gesture. The [demo source](https://github.com/grafloria/grafloria/tree/main/demos) shows the same engine underneath the bindings.
