Learn / JavaScript in 10 minutes
JavaScript in 10 minutes
No framework, no build step required. Two front doors — a render() function and a <grafloria-flow> element — over the same engine that powers every framework binding. The same code the CI-gated demo gallery runs.
1 — Install and render
npm install @grafloria/element @grafloria/renderer @grafloria/engine
render(spec, target) mounts a full editor into any sized element —
spec first, target second:
import { render } from '@grafloria/element';
const api = render({
nodes: [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
],
edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, document.getElementById('canvas'));
Drag, connect, pan, zoom — all live. The return value is the
DiagramInstance, your handle to everything else on this page. Two rules:
the target must have a real height (#canvas { height: 80vh }), and the
spec is data, not text — Mermaid text has its own door (step 7).
label and
data: { label } both work. Every node is born with four bi-directional
ports (top/right/bottom/left), so connections work with zero port ceremony —
sourceHandle: 'bottom' targets a default port by side name.2 — Or: the zero-build tag
The same canvas as a custom element — JSON attributes in, DOM events out. This is the CMS/notebook/plain-HTML path:
<script type="module">import '@grafloria/element';</script>
<grafloria-flow theme="light" fit-view style="height: 80vh"
nodes='[{"id":"a","position":{"x":0,"y":0},"label":"Extract"},
{"id":"b","position":{"x":220,"y":0},"label":"Load"}]'
edges='[{"source":"a","target":"b"}]'>
</grafloria-flow>
<script>
const el = document.querySelector('grafloria-flow');
el.addEventListener('grafloria-connect', (e) => console.log('wired:', e.detail.link));
el.addEventListener('grafloria-nodes-change', (e) => save(e.detail.nodes));
// properties work too: el.nodes = [...]; the engine: el.diagram
</script>
Events (grafloria-ready, -nodes-change,
-edges-change, -selection-change, -connect,
-node-click, -edge-click, -viewport-change) all
bubble and cross shadow boundaries. The element uses light DOM on purpose — your
stylesheet can style node content directly.
3 — Custom nodes without a framework
Two paths. A renderer function:
import { Grafloria } from '@grafloria/element';
Grafloria.registerNodeType('card', (node, el) => {
el.innerHTML = `<div class="card"><b>${node.getData('title')}</b></div>`;
});
// nodes of that type must opt into the HTML layer:
{ id: 'a', type: 'card', custom: true, position: { x: 80, y: 90 },
size: { width: 230, height: 110 }, data: { title: 'Build' } }
…or a zero-JavaScript template slotted into the element — cloned per node,
data-field elements filled from node.data via
textContent (never innerHTML — diagram data is user input):
<grafloria-flow nodes='[{"id":"a","type":"card","custom":true,
"position":{"x":80,"y":90},"size":{"width":230,"height":110},
"data":{"title":"Build"}}]'>
<template data-node-type="card">
<div class="card"><h4 data-field="title"></h4></div>
</template>
</grafloria-flow>
custom: true is always
explicit. The React nodeTypes prop needs it too; Vue slots and Angular
templates flag it for you.4 — Ports and connection rules
const nodes = [
{ id: 'src', position: { x: 120, y: 260 }, size: { width: 130, height: 70 }, label: 'number src',
ports: [{ id: 'out', side: 'right', type: 'output', dataType: 'number' }] },
{ id: 'num', position: { x: 640, y: 140 }, size: { width: 130, height: 70 }, label: 'number in',
ports: [{ id: 'nin', side: 'left', type: 'input', dataType: 'number' }] },
];
// declarative type compatibility…
import { portTypeRegistry, registerConnectionValidator } from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
// …and custom rules: true allows, a string vetoes with a reason
const dispose = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (sourcePort?.type === 'output' && targetPort?.type === 'output')
return 'an output cannot feed another output';
return true;
});
// ports show on hover by default; keep them always visible:
api.getEngine().setInteractionConfig({ portVisibility: 'always' });
5 — Undo, layout, plugins
// ⌘Z / Ctrl+Z already works. For buttons:
await api.getEngine().undo();
await api.getEngine().redo();
api.getEngine().canUndo();
// auto-layout — ELK lazy-loads (~432 KB gz) only on first use
await api.getEngine().layout('elk');
api.renderNow();
api.fitView(40);
// minimap + zoom controls + dotted background, lazy-loaded
import { attachCanvasPlugins } from '@grafloria/element';
attachCanvasPlugins(api, { background: { variant: 'dots' }, minimap: true, controls: true });
Layout names: auto, elk, dagre,
layered, tree, grid, circular,
radial, force, spectral, community.
6 — Save and load, losslessly
import { DiagramSerializer, fromDocument, render } from '@grafloria/element';
// save
const json = JSON.stringify(new DiagramSerializer().serialize(api.getModel()));
// …later, in a fresh page — fromDocument() restores ports, metadata, everything:
const api2 = render(fromDocument(json), host);
7 — Mermaid text and image export
// text round-trip — valid Mermaid out, sidecar keeps your positions
const text = api.exportText();
api.loadText(text); // reconciles into the live canvas
// or at the model level:
import { importDiagramText } from '@grafloria/element';
const r = importDiagramText('flowchart LR\n a[Start] --> b[Ship]');
if (r.unsupported) console.warn('not a supported type:', r.unsupported);
// images — exports the scene graph, not a screenshot
const png = await api.export('png', { scale: 2 }); // data: URL
const svg = await api.export('svg'); // raw SVG string
const pdf = await api.export('pdf'); // real vector PDF
Where next
- The demo gallery — 111 pages; view-source is the tutorial.
- Kits —
erDiagram(),umlDiagram(),dashboard(): data in, a full interactive diagram out. - The model — the engine underneath, which also runs in Node and workers.