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:

main.js
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).

Position is the only required node field. 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:

index.html
<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>
Plain JS is the one surface where custom: true is always explicit. The React nodeTypes prop needs it too; Vue slots and Angular templates flag it for you.

Live: custom node demos →

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.
  • KitserDiagram(), umlDiagram(), dashboard(): data in, a full interactive diagram out.
  • The model — the engine underneath, which also runs in Node and workers.