Use diagram text when you want a file you can review in git and a canvas you can edit by dragging. Load Mermaid into a mounted instance, export the edited document with its sidecar, or import an existing draw.io file. The examples below show Plan → Build → Ship beside an editable text pane.
The text body is Mermaid-compatible. The %%grafloria:document comment carries the document data that Mermaid cannot express, and %%grafloria:body-hash detects edits to the body. Mermaid consumers ignore these comments; Grafloria reads them back.
1. Install the binding you use
Run the matching command in your browser application's project.
For JavaScript:
bashnpm install @grafloria/element @grafloria/engine @grafloria/renderer
For Angular:
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer @grafloria/element rxjs
For Qwik:
bashnpm install @grafloria/qwik @builder.io/qwik @grafloria/engine @grafloria/renderer @grafloria/element
For React:
bashnpm install @grafloria/react react react-dom @grafloria/engine @grafloria/renderer @grafloria/element
For Vue:
bashnpm install @grafloria/vue vue @grafloria/engine @grafloria/renderer @grafloria/element
2. Share the data and import helpers
Use NodeSpec and EdgeSpec for the initial data. Keep the mounted DiagramInstance as the editing surface: exportText() returns a string, and loadText() reconciles text into its live model.
importDrawio accepts plain <mxGraphModel> XML and compressed or uncompressed <mxfile> documents. exportDiagramText converts its imported model to sidecar-carrying text. Loading that text uses the same instance API as the Mermaid editor, without projecting away ports, groups or styles.
Save this file alongside your component or entry point. The helpers return status text for your UI; they do not print a service response or replace the mounted canvas.
tsimport type { DiagramInstance, NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { importDrawio, exportDiagramText } from '@grafloria/engine';
export const nodes: NodeSpec[] = [
{ id: 'plan', label: 'Plan', position: { x: 40, y: 80 }, size: { width: 140, height: 60 } },
{ id: 'build', label: 'Build', position: { x: 260, y: 80 }, size: { width: 140, height: 60 } },
{ id: 'ship', label: 'Ship', position: { x: 480, y: 80 }, size: { width: 140, height: 60 } },
];
export const edges: EdgeSpec[] = [
{ id: 'e1', source: 'plan', target: 'build' },
{ id: 'e2', source: 'build', target: 'ship' },
];
export function applyText(api: DiagramInstance, text: string): string {
try {
const result = api.loadText(text);
api.renderNow();
api.fitView(40);
return result.source === 'sidecar'
? 'Loaded the document sidecar'
: result.sidecarMerged
? 'Applied the body edit onto the sidecar document'
: 'Parsed Mermaid text';
} catch (error) {
return error instanceof Error ? error.message : 'Text import failed';
}
}
export async function applyDrawio(api: DiagramInstance, text: string): Promise<string> {
const result = await importDrawio(text);
if (!result.diagram) return result.error ?? 'No readable first page';
const status = applyText(api, exportDiagramText(result.diagram));
return [status, ...result.warnings].join('\n');
}
3. Mount the editor
Choose one binding. Each editor exports its initial canvas into the textarea. Edit build[Build] to build[Verify], keeping the sidecar comments, then press Load Mermaid: the middle box reads Verify and the surviving nodes retain their sidecar geometry. Press Export after dragging a node to capture its new arrangement. Paste XML, or choose a .drawio file, then press Import draw.io to replace the canvas content with the first imported page.
These editors add a text snapshot and explicit export and import actions to the JavaScript render, React GrafloriaFlow, Vue GrafloriaFlow and Qwik GrafloriaFlow mounts; see group and nest nodes for mounting and instance storage.
Angular exposes DiagramCanvasComponent. Its text loader differs from the renderer instance's loader:
Known issue: Angular's
loadText(text)applies parser output without rejecting unsupported or erroneous bodies, and does not adopt diagram-level grammar metadata or replace existing groups. Until it is fixed, validate withimportDiagramText()and replace the canvas's active diagram with the parsed model.
The intended Angular call is this.canvas().loadText(this.text). The Angular tab implements the workaround with importDiagramText and DiagramEngine, so an invalid import leaves the current diagram intact and a non-flowchart retains its export grammar. It explicitly schedules a repaint after loading.
tsimport { render } from '@grafloria/element';
import { nodes, edges, applyText, applyDrawio } from './diagram-text';
const root = document.createElement('section');
document.body.append(root);
const host = document.createElement('div');
host.style.height = '400px';
const source = document.createElement('textarea');
source.rows = 10;
source.style.width = '100%';
const status = document.createElement('pre');
const file = document.createElement('input');
file.type = 'file';
file.accept = '.drawio,.xml';
const exportButton = document.createElement('button');
exportButton.textContent = 'Export';
const loadButton = document.createElement('button');
loadButton.textContent = 'Load Mermaid';
const drawioButton = document.createElement('button');
drawioButton.textContent = 'Import draw.io';
root.append(host, source, exportButton, loadButton, file, drawioButton, status);
const api = render({ nodes, edges }, host);
api.renderNow();
api.fitView(40);
source.value = api.exportText();
exportButton.onclick = () => { source.value = api.exportText(); };
loadButton.onclick = () => { status.textContent = applyText(api, source.value); };
file.onchange = async () => {
const selected = file.files?.[0];
if (selected) source.value = await selected.text();
};
drawioButton.onclick = async () => {
status.textContent = await applyDrawio(api, source.value);
};
// Call this when your application removes this editor.
export function unmountEditor(): void {
api.dispose();
root.remove();
}
tsimport { AfterViewInit, Component, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { DiagramEngine, importDiagramText, importDrawio } from '@grafloria/engine';
import { nodes, edges } from './diagram-text';
@Component({
selector: 'app-root',
standalone: true,
imports: [DiagramCanvasComponent, FormsModule],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:400px" />
<textarea [(ngModel)]="text" rows="10" style="width:100%"></textarea>
<button (click)="exportText()">Export</button>
<button (click)="load()">Load Mermaid</button>
<input #file type="file" accept=".drawio,.xml" (change)="readFile(file)" />
<button (click)="drawio()">Import draw.io</button>
<pre>{{ status }}</pre>
`,
})
export class AppComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes = nodes;
edges = edges;
text = '';
status = '';
ngAfterViewInit(): void { this.exportText(); }
exportText(): void { this.text = this.canvas().exportText(); }
load(): void {
if (!this.text.trim()) { this.status = 'Enter diagram text'; return; }
try {
const result = importDiagramText(this.text);
if (result.unsupported || result.errors?.length) {
this.status = result.unsupported ?? result.errors?.join('\n') ?? 'Unreadable text';
return;
}
const engine: DiagramEngine | undefined = this.canvas().activeEngine();
if (!engine) { this.status = 'Canvas is not ready'; return; }
engine.setDiagram(result.diagram);
this.canvas().scheduleRender();
this.status = result.sidecarMerged ? 'Applied the body edit onto the sidecar document' : `Loaded from ${result.source}`;
} catch (error) {
this.status = error instanceof Error ? error.message : 'Text import failed';
}
}
async readFile(input: HTMLInputElement): Promise<void> {
const selected = input.files?.[0];
if (selected) this.text = await selected.text();
}
async drawio(): Promise<void> {
const result = await importDrawio(this.text);
if (!result.diagram) { this.status = result.error ?? 'No readable first page'; return; }
const engine: DiagramEngine | undefined = this.canvas().activeEngine();
if (!engine) { this.status = 'Canvas is not ready'; return; }
engine.setDiagram(result.diagram);
this.canvas().scheduleRender();
this.status = ['Imported first page', ...result.warnings].join('\n');
}
}
tsximport { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges, applyText, applyDrawio } from './diagram-text';
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
const text = useSignal('');
const status = useSignal('');
return <section>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} fitView
style={{ height: '400px' }} onInit$={$((api: DiagramInstance) => {
instance.value = noSerialize(api);
text.value = api.exportText();
})} />
<textarea rows={10} style={{ width: '100%' }} value={text.value}
onInput$={(_, el) => { text.value = el.value; }} />
<button onClick$={() => { if (instance.value) text.value = instance.value.exportText(); }}>Export</button>
<button onClick$={() => { if (instance.value) status.value = applyText(instance.value, text.value); }}>Load Mermaid</button>
<input type="file" accept=".drawio,.xml" onChange$={async (_, el) => {
const selected = el.files?.[0];
if (selected) text.value = await selected.text();
}} />
<button onClick$={async () => {
if (instance.value) status.value = await applyDrawio(instance.value, text.value);
}}>Import draw.io</button>
<pre>{status.value}</pre>
</section>;
});
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges, applyText, applyDrawio } from './diagram-text';
export default function App() {
const instance = useRef<DiagramInstance | null>(null);
const [text, setText] = useState('');
const [status, setStatus] = useState('');
return <section>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} fitView
style={{ height: 400 }} onInit={(api) => {
instance.current = api;
setText(api.exportText());
}} />
<textarea rows={10} style={{ width: '100%' }} value={text}
onChange={(event) => setText(event.target.value)} />
<button onClick={() => { if (instance.current) setText(instance.current.exportText()); }}>Export</button>
<button onClick={() => { if (instance.current) setStatus(applyText(instance.current, text)); }}>Load Mermaid</button>
<input type="file" accept=".drawio,.xml" onChange={async (event) => {
const selected = event.target.files?.[0];
if (selected) setText(await selected.text());
}} />
<button onClick={async () => {
if (instance.current) setStatus(await applyDrawio(instance.current, text));
}}>Import draw.io</button>
<pre>{status}</pre>
</section>;
}
vue<script setup lang="ts"> import { ref, shallowRef } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/renderer'; import { nodes, edges, applyText, applyDrawio } from './diagram-text'; const instance = shallowRef<DiagramInstance>(); const text = ref(''); const status = ref(''); function onInit(api: DiagramInstance): void { instance.value = api; text.value = api.exportText(); } function exportText(): void { if (instance.value) text.value = instance.value.exportText(); } function load(): void { if (instance.value) status.value = applyText(instance.value, text.value); } async function readFile(event: Event): Promise<void> { const input = event.target; if (!(input instanceof HTMLInputElement)) return; const selected = input.files?.[0]; if (selected) text.value = await selected.text(); } async function drawio(): Promise<void> { if (instance.value) status.value = await applyDrawio(instance.value, text.value); } </script> <template> <section> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" fit-view style="height:400px" @init="onInit" /> <textarea v-model="text" rows="10" style="width:100%"></textarea> <button @click="exportText">Export</button> <button @click="load">Load Mermaid</button> <input type="file" accept=".drawio,.xml" @change="readFile" /> <button @click="drawio">Import draw.io</button> <pre>{{ status }}</pre> </section> </template>
The text pane is an explicit snapshot, not an automatic mirror of every canvas gesture. Press Export to refresh it. The framework components own their canvas teardown; the JavaScript sample exposes an unmount function for its host application.
The Angular editor starts with the same nodes and exported text.
The Qwik editor holds the text as serializable state beside its live instance.
The React editor exposes the same explicit export and load workflow.
The Vue editor also starts with the exported body and document sidecar in its textarea.
Try the live Mermaid round-trip editor or the draw.io importer.
4. Compose architecture text
Paste this source into any editor above and press Load Mermaid. Cloud becomes a region containing Web app and API. The sided line exits Web app on the right and enters API on the left; Grafloria uses those sides to arrange the services. No custom layout plug-in is needed.
mermaidarchitecture-beta group cloud(cloud)[Cloud] service web(server)[Web app] in cloud service api(server)[API] in cloud web:R -[HTTPS]-> L:api
For a row-and-column composition, paste this instead. columns 3 defines the grid, :3 spans a row, and space leaves a hole.
mermaidblock-beta columns 3 title["Application"]:3 web["Web app"] space api["API"] db[("Database")]:3 web --> api api --> db
Press Export to obtain text in the diagram's grammar, then Load Mermaid to read that exported text back. See the live architecture and block demo for nested regions and layered grids.
Supported text and import results
The text format reads and writes these families:
| Header | Use it for |
|---|---|
flowchart, graph | Nodes, labeled edges and subgraphs |
erDiagram | Entities and relationships |
classDiagram | Classes and relationships |
stateDiagram, stateDiagram-v2 | States and transitions |
architecture-beta | Services, regions and sided lines |
block-beta | Grids, spans, spaces and nested blocks |
The parser returns an ImportTextResult. Inspect source to distinguish a sidecar load from a body parse, bodyEdited for a hash mismatch, and sidecarMerged for an edit applied onto the sidecar base. sidecarInvalid identifies a sidecar whose JSON cannot be parsed. unsupported names a recognized but unsupported type, such as sequenceDiagram; errors lists parser diagnostics with line information.
Standalone importDiagramText() is best effort: readable lines can produce a partial model. The renderer instance's loadText() rejects empty text, unsupported types and body parse errors before changing the canvas. The sample catches that error and displays its message. Angular needs the validation workaround above.
For draw.io, show warnings even when a diagram imports successfully: each warning names a dropped or approximated construct. Unreadable input returns error, not a thrown exception. Multi-page files expose pages; each page has its own name, diagram, warnings and optional error. The samples display the first page. For a page picker, select a readable entry from pages and load exportDiagramText(page.diagram) through the same helper; a corrupt later page does not invalidate the other pages.
Options and round-trip boundaries
Pass these options to the instance's text methods, or to the corresponding engine text functions:
| Option | Type | Default | What it does |
|---|---|---|---|
exportText({ lossless }) | boolean | true | Includes the document sidecar and body hash; false returns only the Mermaid body. |
exportText({ positions }) | boolean | false | Writes readable %%grafloria:at comments for exact node and zone positions and sizes. |
loadText(text, { prefer }) | 'auto' | 'sidecar' | 'text' | 'auto' | Uses the sidecar unless the body hash detects an edit; 'sidecar' ignores body edits; 'text' ignores the sidecar. |
Keep both sidecar comments for a document round trip. In automatic mode, a body edit changes structure, labels and shapes on the sidecar base; surviving nodes retain their geometry, styles and ports. Forcing 'text', or exporting with lossless: false, crosses the best-effort boundary: pure Mermaid does not carry all data bags, port geometry, group nesting or viewport data.
“Lossless” refers to document content, not the viewing session. Text export strips node/link selection, hover and focus state and derived routed link points; manual waypoints remain. The renderer instance's loadText() reconciles content and grammar metadata, but does not restore the sidecar's camera into the mounted viewport. The JavaScript, React, Vue and Qwik editors deliberately frame the imported content with fitView(40) instead.
Known issue: The renderer instance's
loadText()does not transfer saved comment threads or non-grammar diagram-level metadata into the mounted model, even though the sidecar and the imported model contain them. Until it is fixed, use the shared saved-document workflow rather than this text loader for documents that need those fields preserved.
The intended round trip is api.loadText(api.exportText()). In the current renderer, that call transfers nodes, links, groups, strokes and only the listed grammar metadata keys; it does not replace arbitrary diagram metadata or restore saved comments. These text editors therefore do not provide a complete document round trip for comment-bearing or application-metadata-bearing diagrams. Use save and restore documents for those diagrams.
draw.io is an import-only format here. Export the migrated canvas as Grafloria text or a shared saved document, not as a .drawio file. For document loading into a new mount, use fromDocument, which restores models and supplies the mounted wiring for groups and kits. Avoid hand-projecting an imported model to a short node/edge tuple; see save and restore documents.
Related
- Lay out a diagram — rearrange a larger imported graph.
- Commands and history — distinguish document loading from undoable user edits.
- Theme a canvas — configure the imported canvas's appearance and sizing.
- Mermaid viewer — try the supported diagram families.
Was this page helpful?