Skip to content
D
Documentation

Round-trip Mermaid text

how-to
1 min readUpdated

Use Mermaid text as an editable view of a live Grafloria diagram: export the current model, edit the visible Mermaid body, then load it back while retaining model data that Mermaid cannot express, including positions.

When to use this

Use the instance methods after mounting a canvas. DiagramInstance is the shared handle for the rendered model. exportText() returns Mermaid-compatible text, and loadText() parses that text and reconciles it into the same instance.

Use the framework bindings

Each binding exposes the same instance operations after its component mounts. Keep the instance in the binding’s initialization callback or hook; do not recreate it in a render function.

JavaScript

js
import { render } from '@grafloria/element';

const stage = document.getElementById('stage');
const source = document.getElementById('source');
const instance = render({
  nodes: [
    { id: 'start', label: 'Start', position: { x: 80, y: 60 }, size: { width: 140, height: 60 } },
    { id: 'work', label: 'Work', position: { x: 320, y: 60 }, size: { width: 140, height: 60 } },
    { id: 'done', label: 'Done', position: { x: 560, y: 60 }, size: { width: 140, height: 60 } },
  ],
  edges: [
    { id: 'e1', source: 'start', target: 'work' },
    { id: 'e2', source: 'work', target: 'done' },
  ],
}, stage);

instance.renderNow();
source.value = instance.exportText();
document.getElementById('load').addEventListener('click', () => {
  instance.loadText(source.value);
  instance.renderNow();
});
html
<div id="stage" style="height:400px"></div>
<textarea id="source" style="width:100%;height:220px"></textarea>
<button id="load">Load</button>

The canvas shows Start → Work → Done, and the textarea contains the exported text. Edit the textarea and choose Load to apply the text to the same mounted instance.

Angular

ts
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DiagramCanvasComponent } from '@grafloria/angular';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent, FormsModule],
  template: `
    <div style="height:400px"><grafloria-diagram-canvas [nodes]="nodes" [edges]="edges" /></div>
    <textarea [(ngModel)]="text" style="width:100%;height:220px"></textarea>
    <button (click)="exportText()">Export</button><button (click)="loadText()">Load</button>
  `,
})
export class MermaidTextComponent implements AfterViewInit {
  canvas = viewChild.required(DiagramCanvasComponent);
  text = '';
  nodes = [
    { id: 'start', position: { x: 80, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
    { id: 'work', position: { x: 320, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Work' } },
    { id: 'done', position: { x: 560, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Done' } },
  ];
  edges = [{ id: 'e1', source: 'start', target: 'work' }, { id: 'e2', source: 'work', target: 'done' }];
  exportText(): void { this.text = this.canvas().exportText(); }
  loadText(): void { this.canvas().loadText(this.text); }
  ngAfterViewInit(): void { this.exportText(); }
}

The canvas renders Start → Work → Done. Export fills the textarea; edit it and choose Load to reconcile the edit into the mounted canvas.

Qwik

tsx
import { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance, type NodeSpec, type EdgeSpec } from '@grafloria/qwik';

const nodes: NodeSpec[] = [
  { id: 'start', position: { x: 80, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
  { id: 'work', position: { x: 320, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Work' } },
  { id: 'done', position: { x: 560, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Done' } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'start', target: 'work' }, { id: 'e2', source: 'work', target: 'done' }];

export default component$(() => {
  const instance = useSignal<NoSerialize<DiagramInstance>>();
  const text = useSignal('');
  return <div style={{ height: '400px' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
    onInit$={$((api: DiagramInstance) => { instance.value = noSerialize(api); text.value = api.exportText(); })} />
    <button onClick$={() => { if (instance.value) text.value = instance.value.exportText(); }}>Export</button>
    <button onClick$={() => { instance.value?.loadText(text.value); }}>Load</button>
    <textarea value={text.value} onInput$={(_, element) => { text.value = element.value; }} />
  </div>;
});

The mounted GrafloriaFlow shows the three nodes. Load applies edited text; Export reads the current model back into the textarea.

React

tsx
import { useRef, useState } from 'react';
import { GrafloriaFlow, type DiagramInstance, type NodeSpec, type EdgeSpec } from '@grafloria/react';

const nodes: NodeSpec[] = [
  { id: 'start', position: { x: 80, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
  { id: 'work', position: { x: 320, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Work' } },
  { id: 'done', position: { x: 560, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Done' } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'start', target: 'work' }, { id: 'e2', source: 'work', target: 'done' }];

export default function MermaidTextDemo() {
  const instance = useRef<DiagramInstance | null>(null);
  const [text, setText] = useState('');
  return <div style={{ height: 400 }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} style={{ height: '100%' }}
    onInit={(api) => { instance.current = api; setText(api.exportText()); }} />
    <button onClick={() => { if (instance.current) setText(instance.current.exportText()); }}>Export</button>
    <button onClick={() => { instance.current?.loadText(text); }}>Load</button>
    <textarea value={text} onChange={(event) => setText(event.target.value)} style={{ width: '100%', height: 220 }} />
  </div>;
}

The component renders Start → Work → Done and keeps the live instance in a ref. Editing the controlled textarea and selecting Load changes the mounted diagram.

Vue

vue
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow, type DiagramInstance, type NodeSpec, type EdgeSpec } from '@grafloria/vue';
const text = ref('');
let instance: DiagramInstance | null = null;
const nodes: NodeSpec[] = [
  { id: 'start', position: { x: 80, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Start' } },
  { id: 'work', position: { x: 320, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Work' } },
  { id: 'done', position: { x: 560, y: 60 }, size: { width: 140, height: 60 }, data: { label: 'Done' } },
];
const edges: EdgeSpec[] = [{ id: 'e1', source: 'start', target: 'work' }, { id: 'e2', source: 'work', target: 'done' }];
function onInit(api: DiagramInstance) { instance = api; text.value = api.exportText(); }
function exportText() { if (instance) text.value = instance.exportText(); }
function loadText() { instance?.loadText(text.value); }
</script>
<template><div style="height:400px"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" style="height:100%" />
  <button @click="exportText">Export</button><button @click="loadText">Load</button><textarea v-model="text" style="width:100%;height:220px" /></div></template>

The mounted component shows Start → Work → Done. v-model supplies edited Mermaid text to loadText(); exportText() reads it back from the same instance.

Inspect text before mounting

Use importDiagramText when you need to parse text before deciding how to mount it. It returns an ImportTextResult. The live canvas path remains loadText() because it reconciles the result into a rendered instance.

See it running

Open the Mermaid text demo to edit Mermaid text and apply it to the canvas. Its source follows the same flow.

Was this page helpful?

Round-trip Mermaid text — Grafloria