Use this when a saved diagram must come back as the same editable document, including its nodes, links, groups, ports, and metadata. Save the live model, then load the saved document with fromDocument before mounting a fresh instance.
Use the lossless document path
render returns the live DiagramInstance. Its model is the document source; DiagramSerializer converts that model to a plain object or a portable envelope.
This complete TypeScript example renders two real diagrams. The first has a custom port, metadata, a link, and a group. The second is mounted from the saved JSON, so the restored models—not a reduced node-spec projection—go back into the renderer.
tsimport { DiagramSerializer } from '@grafloria/engine';
import { fromDocument, render } from '@grafloria/element';
const originalHost = document.getElementById('original')!;
const restoredHost = document.getElementById('restored')!;
originalHost.style.height = '300px';
restoredHost.style.height = '300px';
const original = render({
nodes: [
{
id: 'author',
label: 'Author',
position: { x: 60, y: 80 },
size: { width: 150, height: 66 },
metadata: { role: 'writer' },
ports: [{ id: 'author-out', side: 'right', type: 'output', dataType: 'document' }],
},
{
id: 'review',
label: 'Review',
position: { x: 320, y: 80 },
size: { width: 150, height: 66 },
},
],
edges: [{ id: 'author-review', source: 'author', target: 'review', sourceHandle: 'author-out' }],
groups: [{ id: 'workflow', label: 'Workflow', children: ['author', 'review'], padding: 24 }],
}, originalHost);
const serializer = new DiagramSerializer();
const savedJson = JSON.stringify(
serializer.serializeEnvelope(original.getModel(), { generator: 'my-diagram-app' }),
);
const loaded = fromDocument(savedJson);
const restored = render({ nodes: [], edges: [] }, restoredHost);
restored.setNodes(loaded.nodes);
restored.setEdges(loaded.edges);
restored.setGroups(loaded.model.getGroups());
restored.renderNow();
restored.fitView();
const restoredAuthor = loaded.model.getNode('author')!;
console.log(restoredAuthor.getMetadata('role')); // 'writer'
console.log(restoredAuthor.getPort('author-out') !== undefined); // true
console.log(loaded.model.getGroups().length); // 1
console.log(restored.getModel().getLinks().length); // 1
Give both hosts a height; otherwise the renderer has no drawing area:
html<div id="original" style="height: 300px"></div>
<div id="restored" style="height: 300px"></div>
The second canvas shows the same two nodes and link inside the restored group. The metadata and named port remain on the live restored model. serializeEnvelope() adds generator and integrity information; deserialize() verifies an envelope checksum and rejects corrupted data instead of loading it silently.
Keep the framework binding at the front door
Each binding gives you the same live instance. Use its initialization callback to obtain the model, then apply the serializer/from-document pattern above when the saved document is authoritative. The examples below show the binding-specific mount and instance hand-off; each canvas has a size.
JavaScript
The render() example above is the JavaScript front door. Save from instance.getModel(), and mount a fresh host with render(fromDocument(savedJson), host).
Angular
tsimport { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { DiagramSerializer } from '@grafloria/engine';
import type { SerializedDiagram } from '@grafloria/engine';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: '<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:400px" />',
})
export class SavedDiagramComponent {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes = [{ id: 'a', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, label: 'Author' }];
edges = [];
saved: SerializedDiagram | null = null;
private readonly serializer = new DiagramSerializer();
ngAfterViewInit(): void {
this.saved = this.canvas().snapshot();
}
save(): void { this.saved = this.canvas().snapshot(); }
restore(): void {
if (this.saved) this.canvas().loadSnapshot(this.saved);
}
}
The Angular binding's snapshot methods restore the full document through the canvas instance. For a portable document shared with another host, serialize the instance's model and mount fromDocument() with the core render() entry point.
React
tsximport { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';
export default function SavedDiagram() {
const nodes = [{ id: 'a', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, label: 'Author' }];
const edges: Array<{ id: string; source: string; target: string }> = [];
return <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
onInit={(api: DiagramInstance) => { api.getModel(); }}
style={{ display: 'block', height: '400px' }} />;
}
When instance.current is initialized, pass instance.current.getModel() to DiagramSerializer. Do not rebuild a loaded document by mapping nodes to { id, position, size, label }; that projection omits ports and metadata.
Vue
vue<script setup lang="ts"> import { ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; const api = ref<DiagramInstance | null>(null); const nodes = [{ id: 'a', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, label: 'Author' }]; const edges: Array<{ id: string; source: string; target: string }> = []; </script> <template> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="(instance) => api = instance" style="display:block;height:400px" /> </template>
Use api after @init to serialize its model. A saved document that must retain structure belongs in fromDocument() plus render(), rather than in a reduced controlled-spec array.
Qwik
tsximport { component$, $ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
export default component$(() => {
const nodes = [{ id: 'a', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, label: 'Author' }];
const edges: Array<{ id: string; source: string; target: string }> = [];
return <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
onInit$={$((instance: DiagramInstance) => { instance.getModel(); })}
style={{ display: 'block', height: '400px' }} />;
});
Once api.value exists, serialize api.value.getModel(). The lossless reload still uses the core fromDocument() result as the input to render().
Portable envelopes and options
Use the envelope for new persistence, especially when documents cross storage or service boundaries.
| Option | Type | Default | What it does |
|---|---|---|---|
generator | string | — | Identifies the writer in the envelope. |
generatorVersion | string | — | Records the writer's document target version. |
checksum | boolean | true | Adds an integrity checksum to the envelope. |
createdAt | string | current timestamp | Overrides the creation timestamp, useful for deterministic exports. |
interactive | boolean | true | Re-attaches kit interaction wiring during fromDocument(). Set it to false for a read-only viewer. |
Pitfalls
- Do not use
setNodes()with a hand-built projection for a lossless restore. It can preserve visible boxes while dropping declared ports, metadata, and kit wiring. - Do not treat
v-model:nodesor another controlled node array as the persistence format. It is a projection of node specs, not the full document. - Restore into a fresh host when the saved document is the source of truth. Reusing live model identities can retain state from the previous document.
- Reattach application-owned custom-node painters through
FromDocumentOptions.renderCustomNodeorrenderWidget; functions cannot be serialized.
See the live save-and-restore demo and its source.
Related: The model and document, the DiagramInstance, and export diagrams.
Was this page helpful?