Learn / Vue 3 in 10 minutes
Vue 3 in 10 minutes
From npm install to a flow editor with slot-defined nodes, connection rules, undo and export. The same code the Vue demo gallery runs.
1 — Install and mount a canvas
npm install @grafloria/vue @grafloria/element @grafloria/renderer @grafloria/engine
One component: <GrafloriaFlow>. Give its wrapper a real height —
the canvas fills its container, and 100% of zero is a blank page.
<script setup>
import { GrafloriaFlow } from '@grafloria/vue';
const 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' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
</script>
<template>
<div style="height: 100vh">
<GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :plugins="true" />
</div>
</template>
Drag, connect, pan, zoom — plus minimap, zoom controls and a dotted background from
:plugins="true" (lazy-loaded). default-nodes is the
uncontrolled form; for app-owned state use v-model:nodes /
v-model:edges with ref()s — the canvas emits spec updates
after every add/remove. No stylesheet import needed.
2 — Custom nodes are slots
A node whose type is card renders through
#node-card — declaring the slot is the opt-in, no flag needed.
Slot content is real Vue: reactivity, components and event handlers all work.
<template>
<div style="height: 100vh">
<GrafloriaFlow :default-nodes="nodes" :default-edges="edges">
<template #node-card="{ data }">
<div class="card">
<div class="title">{{ data.title }}</div>
<div class="owner">owner: {{ data.owner }}</div>
</div>
</template>
</GrafloriaFlow>
</div>
</template>
<style scoped>
.card { height: 100%; background: #fff; border: 1.5px solid #94A5F0;
border-radius: 12px; padding: 10px 14px; box-sizing: border-box; }
.title { font-weight: 700; } .owner { font-size: 12px; color: #5A6478; }
</style>
const nodes = [
{ id: 'a', type: 'card', position: { x: 80, y: 90 }, size: { width: 230, height: 110 },
data: { title: 'Build', owner: 'CI' } },
{ id: 'b', type: 'card', position: { x: 430, y: 90 }, size: { width: 230, height: 110 },
data: { title: 'Deploy', owner: 'CD' } },
];
Slot context: { node, data, engine } — node is the live
NodeModel. #node (no type) is the wildcard. Give the slot
root height: 100% so it fills the node box.
Live: slot-defined custom nodes →
3 — Ports and connection rules
<script setup>
import { onBeforeUnmount } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';
import { registerConnectionValidator, clearConnectionValidators, portTypeRegistry } from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
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' }] },
];
let dispose;
function onInit(instance: DiagramInstance) {
dispose = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (sourcePort?.type === 'output' && targetPort?.type === 'output')
return 'an output cannot feed another output';
return true;
});
instance.getEngine().setInteractionConfig({ portVisibility: 'always' });
}
onBeforeUnmount(() => { dispose?.(); clearConnectionValidators(); });
</script>
<template>
<div style="height: 100vh">
<GrafloriaFlow :default-nodes="nodes" :default-edges="[]" @init="onInit" />
</div>
</template>
onBeforeUnmount or they
leak across routed pages. A validator returns true to allow, or a string
as the rejection reason.4 — Undo and engine access
⌘Z/Ctrl+Z works with zero wiring. For buttons, capture the
instance in @init and go through the engine:
let instance = null;
function onInit(api) { instance = api; }
async function undo() { await instance?.getEngine().undo(); instance?.renderNow(); }
async function redo() { await instance?.getEngine().redo(); instance?.renderNow(); }
For an inspector panel elsewhere in the tree, wrap the flow in
<GrafloriaProvider> and use the composables —
useGrafloria(), useSelection(), useViewport(),
useOnSelectionChange().
5 — Auto-layout
<GrafloriaFlow :default-nodes="nodes" :default-edges="edges" layout="elk" />
<!-- configured: -->
<GrafloriaFlow :layout="{ name: 'dagre', options: { direction: 'TB', rankSpacing: 80 } }" ... />
Names: auto, elk, dagre, layered,
tree, grid, circular, radial,
force, spectral, community. ELK lazy-loads on
first use. The prop re-runs on value change only — never on node data, so it
won't fight a drag. Template ref: applyLayout('dagre'); event:
@layout-done.
6 — Save, load, export
import { DiagramSerializer, fromDocument } from '@grafloria/element';
// save — the full document, losslessly
const serializer = new DiagramSerializer();
const json = JSON.stringify(serializer.serialize(instance.getModel()));
// restore later with fromDocument() — ports and metadata survive
// (see the JavaScript tutorial, step 6, for the fresh-page form)
// Mermaid text — template ref shortcuts exist too (exportText / loadText)
const text = instance.exportText();
instance.loadText(text);
// images: PNG/PDF return data: URLs, SVG returns the raw string
const png = await instance.export('png', { scale: 2 });
Where next
- Every demo as a Vue SFC — 100+ routes, source shown.
- Kits —
<GrafloriaDiagram :spec="erDiagram(...)">and<GrafloriaDashboard>with#widget-<kind>slots. - The model — what nodes, ports, links and groups really are.