Skip to content
D
Documentation

Model and document

concept
3 min readUpdated

Grafloria keeps one diagram model beneath every framework binding: nodes, links, ports, groups, and viewport data are the persistent meaning of the diagram; rendering and layout turn that meaning into geometry on screen.

How the parts fit together

mermaid
flowchart LR
  S["Node and edge specs"] --> I["DiagramInstance"]
  I --> M["DiagramModel"]
  M --> N["nodes"]
  M --> L["links and ports"]
  M --> G["groups"]
  M --> D["serialized document"]
  D --> F["fromDocument()"]
  F --> I

The framework component supplies initial or controlled specs. The mounted DiagramInstance owns the live diagram and gives you the model through getModel(). Use that model for queries and persistence; use getEngine() for behavior such as layout, validation, and undo.

Nodes identify the things in the diagram

A NodeModel has an ID, a type, geometry, ports, style, and data. The ID is the stable identity used by links and groups. Keep it when you update a node: reconciliation can then update the existing live model instead of treating the item as a new one.

At the spec boundary, a node can carry id, position, size, label, data, and ports. The four default ports are deterministic; add explicit ports when a connection must use a particular direction or port identity. The model exposes those ports for lookup, including getPortBySide() and getPort().

An EdgeSpec names its endpoints with source and target. Those values are node IDs. Set sourceHandle or targetHandle to pin an endpoint to a port; a bare side such as 'right' is also accepted. Without a handle, the renderer chooses the appropriate port-facing attachment as the nodes move.

type, router, connector, labels, styles, and waypoints describe link intent. The model stores that intent, while routing and rendering calculate the visible path. Do not persist a screen-space path as a substitute for the link's endpoints.

Groups add membership and containment

A GroupSpec is a zone with an ID, an optional label, and child IDs. Its bounds can fix the frame; otherwise the frame fits its children using padding. Children become group members and move with the group. Groups can be nested and collapsed, so they are model entities rather than a background rectangle.

The live GroupModel stores membership, collapse state, geometry, and layout configuration. Removing a group through setGroups() keeps its boxes; it removes the zone, not the nodes inside it.

One document is the persistence boundary

The DiagramModel contains maps of nodes, links, and groups. Serialize that live model rather than only the framework's input arrays: gestures and model-level details such as ports, groups, metadata, and waypoints belong to the live model.

DiagramSerializer turns the model into a plain serialized object. serializeEnvelope() wraps it in the portable document envelope when you need generator identity and related document metadata. The document is data; renderer callbacks and other post-render wiring are functions, so they are not in the file and must be supplied again when a host reloads a custom or kit-based diagram.

Save a mounted diagram and mount it again

This browser example creates two visible nodes and one link, saves the first instance's model, and mounts a second instance from the saved JSON. Both hosts have dimensions, so both diagrams paint.

js
import { fromDocument, render } from '@grafloria/element';
import { DiagramSerializer } from '@grafloria/engine';

const nodes = [
  {
    id: 'intake',
    type: 'rect',
    position: { x: 40, y: 60 },
    size: { width: 120, height: 48 },
    label: 'Intake',
  },
  {
    id: 'review',
    type: 'rect',
    position: { x: 260, y: 60 },
    size: { width: 120, height: 48 },
    label: 'Review',
  },
];

const edges = [
  { id: 'intake-to-review', source: 'intake', target: 'review', label: 'checks' },
];

const firstHost = document.getElementById('first-diagram');
const secondHost = document.getElementById('second-diagram');

if (!firstHost || !secondHost) {
  throw new Error('Both diagram hosts are required');
}

firstHost.style.height = '400px';
secondHost.style.height = '400px';

const first = render({ nodes, edges }, firstHost);
const saved = JSON.stringify(new DiagramSerializer().serialize(first.getModel()));
const loaded = fromDocument(saved);
const second = render({ nodes: [], edges: [] }, secondHost);
second.setNodes(loaded.nodes);
second.setEdges(loaded.edges);

first.renderNow();
second.renderNow();

The first call returns the live instance. The serializer reads its model, and fromDocument() converts the saved object or JSON string back into a spec that render can mount. The second canvas therefore receives the saved node IDs, link endpoints, labels, and geometry. Provide any application-owned custom-node painter or kit wiring again when loading a document; those callbacks are not serializable.

Use the instance for the rendered surface and its model for document data. If you only project nodes and edges back into plain specs, details outside that projection can be lost. Use the full document path when ports, groups, metadata, or edited waypoints must survive.

Was this page helpful?