Import Mermaid-compatible text—including a .mmd file—into a live, editable canvas, then export the same diagram back to text without flattening it into an image.
Use the canvas instance: DiagramInstance exposes loadText() and exportText(). Loading reconciles the parsed diagram into the mounted model, so its renderer and listeners remain attached. Exported text includes a Grafloria document sidecar by default: an untouched import/export round-trip retains the full document, while the readable Mermaid body remains available to other Mermaid tools.
Import and export in the browser
Use this pattern when a user supplies Mermaid text or a text file. The file picker accepts .mmd, .mermaid, and .txt; the file contents—not the file wrapper—are passed to the canvas. The JavaScript, Angular, React, and Vue examples start with two real nodes and an edge, then import or export on demand. The Qwik example mounts its editable canvas from typed node and edge specs. Give each canvas a sized parent so it can draw.
JavaScript
jsimport { render } from '@grafloria/element';
const app = document.createElement('main');
app.style.cssText = 'font: 14px sans-serif; padding: 12px;';
const file = document.createElement('input');
file.type = 'file';
file.accept = '.mmd,.mermaid,.txt';
const loadButton = document.createElement('button');
loadButton.type = 'button';
loadButton.textContent = 'Load text';
const exportButton = document.createElement('button');
exportButton.type = 'button';
exportButton.textContent = 'Export text';
const editor = document.createElement('textarea');
editor.setAttribute('aria-label', 'Mermaid diagram text');
editor.style.cssText = 'display:block;box-sizing:border-box;width:100%;height:150px;margin:8px 0;';
const status = document.createElement('p');
const host = document.createElement('div');
host.style.cssText = 'height:420px;width:100%;';
app.append(file, loadButton, exportButton, editor, status, host);
document.body.append(app);
const nodes = [
{ id: 'start', position: { x: 60, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'finish', position: { x: 300, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } },
];
const edges = [{ id: 'next', source: 'start', target: 'finish' }];
const instance = render({ nodes, edges }, host);
editor.value = instance.exportText();
function applyText(source) {
const result = instance.loadText(source);
status.textContent = result.unsupported
? `Unsupported diagram type: ${result.unsupported}`
: `Loaded from ${result.source}${result.bodyEdited ? ' (text body edited)' : ''}.`;
}
loadButton.addEventListener('click', () => applyText(editor.value));
exportButton.addEventListener('click', () => {
editor.value = instance.exportText();
status.textContent = 'Exported the live diagram to Mermaid-compatible text.';
});
file.addEventListener('change', async () => {
const selected = file.files?.[0];
if (!selected) return;
editor.value = await selected.text();
applyText(editor.value);
});
The page mounts a two-node diagram, and Export text fills the editor with its Mermaid-compatible representation and lossless sidecar. Load text or choose a text file to update the same editable canvas. This uses render to mount the spec and the returned instance to do the round-trip.
Angular
tsimport { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div style="display:flex;gap:8px;align-items:center">
<input type="file" accept=".mmd,.mermaid,.txt" (change)="loadFile($event)" />
<button type="button" (click)="loadText()">Load text</button>
<button type="button" (click)="exportText()">Export text</button>
</div>
<textarea aria-label="Mermaid diagram text" [value]="text" (input)="editText($event)"
style="box-sizing:border-box;width:100%;height:150px;margin:8px 0"></textarea>
<p>{{ status }}</p>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;width:100%;height:420px" />
`,
})
export class ImportTextComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes: NodeSpec[] = [
{ id: 'start', position: { x: 60, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'finish', position: { x: 300, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } },
];
edges: EdgeSpec[] = [{ id: 'next', source: 'start', target: 'finish' }];
text = '';
status = 'Export text to begin.';
ngAfterViewInit(): void {
this.text = this.canvas().exportText();
}
editText(event: Event): void {
this.text = (event.currentTarget as HTMLTextAreaElement).value;
}
async loadFile(event: Event): Promise<void> {
const input = event.currentTarget as HTMLInputElement;
const selected = input.files?.[0];
if (!selected) return;
this.text = await selected.text();
this.loadText();
}
loadText(): void {
this.canvas().loadText(this.text);
this.status = 'Applied the Mermaid-compatible text to the canvas.';
}
exportText(): void {
this.text = this.canvas().exportText();
this.status = 'Exported the live diagram to Mermaid-compatible text.';
}
}
Here the mounted DiagramCanvasComponent owns the live diagram. Choosing a text file or clicking Load text reconciles its text into the canvas; Export text writes the live diagram back to the textarea. The Angular canvas methods return text for export and apply text to the existing diagram for import.
Qwik
tsximport { component$ } from '@builder.io/qwik';
import { GrafloriaFlow, type EdgeSpec, type NodeSpec } from '@grafloria/qwik';
const nodes: NodeSpec[] = [
{ id: 'start', position: { x: 60, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'finish', position: { x: 300, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } },
];
const edges: EdgeSpec[] = [{ id: 'next', source: 'start', target: 'finish' }];
export default component$(() => (
<div style={{ width: '100%', height: '420px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
</div>
));
The Qwik GrafloriaFlow mounts the typed specs as an editable canvas. Use loadText() and exportText() on the live instance for the text round-trip described above.
React
tsximport { useRef, useState, type ChangeEvent } from 'react';
import { GrafloriaFlow, type DiagramInstance, type EdgeSpec, type NodeSpec } from '@grafloria/react';
const nodes: NodeSpec[] = [
{ id: 'start', position: { x: 60, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
{ id: 'finish', position: { x: 300, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } },
];
const edges: EdgeSpec[] = [{ id: 'next', source: 'start', target: 'finish' }];
export function ImportTextDiagram() {
const instance = useRef<DiagramInstance | null>(null);
const [text, setText] = useState('');
const [status, setStatus] = useState('The canvas is ready.');
function applyText(source: string): void {
const live = instance.current;
if (!live) return;
const result = live.loadText(source);
setStatus(result.unsupported
? `Unsupported diagram type: ${result.unsupported}`
: `Loaded from ${result.source}${result.bodyEdited ? ' (text body edited)' : ''}.`);
}
async function loadFile(event: ChangeEvent<HTMLInputElement>): Promise<void> {
const selected = event.currentTarget.files?.[0];
if (!selected) return;
const source = await selected.text();
setText(source);
applyText(source);
}
function exportText(): void {
const live = instance.current;
if (!live) return;
setText(live.exportText());
setStatus('Exported the live diagram to Mermaid-compatible text.');
}
return (
<div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input type="file" accept=".mmd,.mermaid,.txt" onChange={loadFile} />
<button type="button" onClick={() => applyText(text)}>Load text</button>
<button type="button" onClick={exportText}>Export text</button>
</div>
<textarea aria-label="Mermaid diagram text" value={text}
onChange={(event) => setText(event.currentTarget.value)}
style={{ boxSizing: 'border-box', width: '100%', height: 150, margin: '8px 0' }} />
<p>{status}</p>
<div style={{ width: '100%', height: 420 }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} onInit={(api) => {
instance.current = api;
setText(api.exportText());
}} />
</div>
</div>
);
}
The React GrafloriaFlow callback keeps the instance in a ref across renders. Load text and file selection both apply the editor contents to that instance; Export text replaces the editor contents with the current diagram's text.
Vue
vue<script setup lang="ts"> import { shallowRef, ref } from 'vue'; import { GrafloriaFlow, type DiagramInstance, type EdgeSpec, type NodeSpec } from '@grafloria/vue'; const nodes: NodeSpec[] = [ { id: 'start', position: { x: 60, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Start' } }, { id: 'finish', position: { x: 300, y: 70 }, size: { width: 140, height: 60 }, data: { label: 'Finish' } }, ]; const edges: EdgeSpec[] = [{ id: 'next', source: 'start', target: 'finish' }]; const instance = shallowRef<DiagramInstance | null>(null); const text = ref(''); const status = ref('The canvas is ready.'); function onInit(api: DiagramInstance): void { instance.value = api; text.value = api.exportText(); } function applyText(source: string): void { const live = instance.value; if (!live) return; const result = live.loadText(source); status.value = result.unsupported ? `Unsupported diagram type: ${result.unsupported}` : `Loaded from ${result.source}${result.bodyEdited ? ' (text body edited)' : ''}.`; } async function loadFile(event: Event): Promise<void> { const input = event.currentTarget as HTMLInputElement; const selected = input.files?.[0]; if (!selected) return; const source = await selected.text(); text.value = source; applyText(source); } function exportText(): void { const live = instance.value; if (!live) return; text.value = live.exportText(); status.value = 'Exported the live diagram to Mermaid-compatible text.'; } </script> <template> <div> <div style="display:flex;gap:8px;align-items:center"> <input type="file" accept=".mmd,.mermaid,.txt" @change="loadFile" /> <button type="button" @click="applyText(text)">Load text</button> <button type="button" @click="exportText">Export text</button> </div> <textarea aria-label="Mermaid diagram text" v-model="text" style="box-sizing:border-box;width:100%;height:150px;margin:8px 0"></textarea> <p>{{ status }}</p> <div style="width:100%;height:420px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" /> </div> </div> </template>
The Vue GrafloriaFlow emits its init callback with the live instance. The shallow ref keeps that instance unproxied; loading changes the canvas, while export replaces the textarea value with a new text representation.
Text format and options
importDiagramText, which loadText() uses, returns an ImportTextResult describing the parsed model, which source won, and whether a sidecar body was edited. If a Mermaid type is recognized but unsupported, unsupported names it instead of guessing at a flowchart. The example status displays that result; unsupported text does not render as a valid diagram.
An exported sidecar keeps the full Grafloria document for an untouched round trip. If a reader edits the Mermaid body, the default auto preference applies that body over the sidecar document: text structure and labels reflect the edit while data Mermaid cannot express, such as positions, styles, ports, and groups, remains from the sidecar. Pure Mermaid text has no sidecar and parses on a best-effort basis.
| Option | Type | Default | What it does |
|---|---|---|---|
prefer on ImportTextOptions | 'auto' | 'sidecar' | 'text' | 'auto' | Chooses which source wins when the text contains both a sidecar and a body. auto uses the sidecar unless the body hash shows a hand edit; sidecar ignores body edits; text parses the body and ignores the sidecar. |
The import reconciles into the existing diagram rather than replacing the instance; listeners, plugins, and the renderer stay attached. It is the model—not a screenshot—that the text sidecar carries. See How Grafloria works for the document and model relationship.
What the demos show
The Mermaid text demo starts with a Mermaid-shaped text pane and a rendered three-node flow. Its sidecar supports an unchanged, lossless round-trip; edit build[Build] to build[Verify] and the canvas reflects the body edit while the other nodes remain.
The Mermaid viewer demo provides a paste-and-apply workflow for supported Mermaid diagram types. It names unsupported types instead of rendering them as a misleading flowchart.
For .drawio XML rather than Mermaid text, see the draw.io import demo. It demonstrates plain and compressed imports, multi-page files, and editable canvas models.
For the separate workflow of embedding a model in an exported SVG or PNG and reopening it as an editable diagram, see the editable round-trip demo.
Pitfalls
loadText()reconciles entities by id. If you need a genuinely fresh document and the incoming text reuses ids, clear the current data before applying it; see Add nodes from palettes for the reconciliation warning.- Untouched sidecar text carries more than Mermaid structure. Removing its
%%grafloriacomments leaves pure Mermaid, which parses best-effort and cannot retain every Grafloria-only detail. - The canvas fills its parent. Keep a real height on the wrapper; otherwise the imported diagram has no visible drawing area.
Related
- Mermaid & the text format — import and export behavior, sidecars, and supported Mermaid syntax.
- The graph model and document — the editable document represented by the sidecar.
- Export diagrams — image and document export options.
Was this page helpful?