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.

App.vue
<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.

App.vue
<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

Ports.vue
<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>
Validators are global — dispose in onBeforeUnmount or they leak across routed pages. A validator returns true to allow, or a string as the rejection reason.

Live: typed ports →

Live: connection validation →

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().

Live: drag, then ⌘Z →

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.

Live: ELK layout →

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 });

Live: save & restore →

Live: PNG/SVG download →

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.