Use this when a Mermaid file needs a live, editable canvas: load supported text into a mounted diagram, edit the text or the canvas, and write Mermaid-compatible text back with Grafloria's layout in its sidecar.
What you get
The live handle is a DiagramInstance. Its loadText() method parses Mermaid-compatible text and reconciles the result into the existing canvas; exportText() returns Mermaid-compatible text with a lossless %%grafloria:document sidecar by default. Mermaid renderers ignore the sidecar, while Grafloria reads it to restore positions, sizes, styles, ports, and groups.
The text body carries the human-editable structure and labels. If a body hash detects a hand edit, the edited body supplies the nodes, edges, labels, and shapes; the sidecar remains the base for information Mermaid cannot express. An untouched export loads from the sidecar. A recognized but unsupported type returns unsupported instead of silently drawing the wrong diagram.
Supported types are flowchart/graph, erDiagram, classDiagram, stateDiagram/stateDiagram-v2, architecture-beta, and block-beta.
Import, edit, and export
- Give the canvas a real height and mount it with
render. Thespecargument is diagram data; Mermaid text enters through the instance's text methods. - Call
loadText()with the Mermaid source. Keep the returned result if you need to inspectsource,bodyEdited,sidecarMerged, orunsupported. - Let the user edit the text, then call
loadText()again. The mounted canvas updates without replacing its listeners, plugins, or renderer. - Call
exportText()after canvas edits or text edits. Save the returned string, including its sidecar, when Grafloria positions must survive the next import.
The following JavaScript sample renders three nodes, loads Mermaid text into that mounted instance, changes Build to Verify, and exports the resulting text.
jsimport { render } from '@grafloria/element';
const host = document.getElementById('canvas');
if (!(host instanceof HTMLElement)) {
throw new Error('Missing #canvas element');
}
host.style.height = '400px';
const instance = render({
nodes: [
{ id: 'start', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'work', position: { x: 320, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Work' } },
{ id: 'done', position: { x: 560, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } },
],
edges: [
{ id: 'e1', source: 'start', target: 'work' },
{ id: 'e2', source: 'work', target: 'done' },
],
}, host);
const source = `flowchart LR
start[Start] --> work[Work]
work --> done[Done]`;
const imported = instance.loadText(source);
if (imported.unsupported) {
throw new Error(`Unsupported diagram: ${imported.unsupported}`);
}
const edited = source.replace('work[Work]', 'work[Verify]');
instance.loadText(edited);
const savedMermaid = instance.exportText();
console.log(savedMermaid);
html<div id="canvas" style="height:400px"></div>
The canvas first shows the imported Start → Work → Done flow, then Start → Verify → Done. savedMermaid contains a Mermaid body and the sidecar that preserves Grafloria geometry.
Use the framework binding
Each binding gives you the same live instance. The following examples use the repository's front-door pattern: initial nodes and edges render in the component, the ready/init callback stores the instance, and a button performs the text round-trip.
jsimport { render } from '@grafloria/element';
const host = document.getElementById('canvas');
if (!(host instanceof HTMLElement)) throw new Error('Missing #canvas');
host.style.height = '400px';
const instance = render({
nodes: [{ id: 'a', position: { x: 60, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'b', position: { x: 300, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } }],
edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, host);
const result = instance.loadText('flowchart LR\n a[Start] --> b[Done]');
if (result.unsupported) throw new Error(result.unsupported);
const text = instance.exportText();
console.log(text);
tsimport { Component, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({
standalone: true,
imports: [DiagramCanvasComponent, FormsModule],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:400px" />
<textarea [(ngModel)]="text"></textarea>
<button type="button" (click)="load()">Load</button>
<button type="button" (click)="export()">Export</button>
`,
})
export class MermaidEditorComponent {
canvas = viewChild.required(DiagramCanvasComponent);
text = 'flowchart LR\n start[Start] --> done[Done]';
nodes = [{ id: 'start', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'done', position: { x: 320, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } }];
edges = [{ id: 'e1', source: 'start', target: 'done' }];
load() { this.canvas().loadText(this.text); }
export() { this.text = this.canvas().exportText(); }
}
tsximport { component$, $, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
export default component$(() => {
const instance = useSignal();
const text = useSignal('flowchart LR\n start[Start] --> done[Done]');
const nodes = [{ id: 'start', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'done', position: { x: 320, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } }];
const edges = [{ id: 'e1', source: 'start', target: 'done' }];
return <div style={{ height: '400px' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit$={$((api: DiagramInstance) => { instance.value = api; text.value = api.exportText(); })} /><textarea value={text.value} onInput$={(_event: Event, element: HTMLTextAreaElement) => { text.value = element.value; }} /><button onClick$={() => instance.value?.loadText(text.value)}>Load</button><button onClick$={() => { if (instance.value) text.value = instance.value.exportText(); }}>Export</button></div>;
});
tsximport { useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/react';
export default function MermaidEditor() {
const [text, setText] = useState('flowchart LR\n start[Start] --> done[Done]');
const nodes = [{ id: 'start', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'done', position: { x: 320, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } }];
const edges = [{ id: 'e1', source: 'start', target: 'done' }];
return <div style={{ height: 400 }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={(api: DiagramInstance) => { api.loadText(text); setText(api.exportText()); }} /><textarea value={text} onChange={(event) => setText(event.target.value)} /></div>;
}
vue<script setup lang="ts"> import { ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/vue'; const instance = ref<DiagramInstance | null>(null); const text = ref('flowchart LR\n start[Start] --> done[Done]'); const nodes = [{ id: 'start', position: { x: 80, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'done', position: { x: 320, y: 80 }, size: { width: 140, height: 60 }, data: { label: 'Done' } }]; const edges = [{ id: 'e1', source: 'start', target: 'done' }]; function load() { instance.value?.loadText(text.value); } function exportText() { if (instance.value) text.value = instance.value.exportText(); } </script> <template> <div style="height:400px"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="instance = $event" /><textarea v-model="text" /><button @click="load">Load</button><button @click="exportText">Export</button></div> </template>
In every tab, Load changes the mounted diagram and Export replaces the editor text with the current diagram's Mermaid text. Give the host or component a height; a heightless canvas has no drawing area.
Parse before loading
Use importDiagramText when you need a model or a support check before choosing what to mount. It returns an ImportTextResult. For an untouched sidecar export, source is sidecar; for a hand-edited body it is text and sidecarMerged reports that Grafloria applied the body onto the sidecar document.
tsimport { importDiagramText } from '@grafloria/engine';
const result = importDiagramText('sequenceDiagram\n Alice->>Bob: Hello');
if (result.unsupported) {
const message = `Unsupported Mermaid type: ${result.unsupported}`;
document.body.append(message);
} else {
console.log(result.diagram.getNodes().length);
}
Do not treat a recognized unsupported type as an empty diagram. Keep the current canvas and report result.unsupported to the user.
Options that affect import
| option | type | default | what it does |
|---|---|---|---|
prefer | 'auto' | 'sidecar' | 'text' | 'auto' | Chooses the sidecar or Mermaid body when both exist. auto uses an unchanged sidecar, while a changed body is applied as text; sidecar forces the document; text forces the parsed body without the sidecar merge. |
Pass the option to either importDiagramText(text, options) or the mounted instance's loadText(text, options).
Live demos
- Mermaid text shows lossless export, hand-edit detection, and sidecar preservation. Its source drives the JavaScript pattern above.
- Mermaid viewer lets you try supported flowcharts and see unsupported types reported.
- Mermaid architecture and block diagrams demonstrates
architecture-betaandblock-betalayout and round-tripping.
Was this page helpful?