DEV Community

Ramy Othman
Ramy Othman

Posted on Originally published at grafloria.com

Build a workflow editor in Vue 3 — the whole thing

Originally published on the Grafloria engineering blog.

Vue gets fewer diagram tutorials than React, and the ones that exist usually stop at "nodes appear on screen". This one goes where real workflow editors go: custom node cards, ports with connection rules, ⌘Z, auto-layout, and a save format — in Vue idiom the whole way. Everything below is running code; it's the same API our Vue demo gallery exercises in CI.

1 — A canvas with taste


import { GrafloriaFlow } from '@grafloria/vue';

const nodes = [
  { id: 'trigger', type: 'step', position: { x: 60, y: 120 },
    size: { width: 220, height: 90 }, data: { title: 'New signup', kind: 'Trigger' } },
  { id: 'email', type: 'step', position: { x: 380, y: 120 },
    size: { width: 220, height: 90 }, data: { title: 'Send welcome email', kind: 'Action' } },
];
const edges = [{ id: 'e1', source: 'trigger', target: 'email' }];

          {{ data.kind }}
          {{ data.title }}

.step { height: 100%; background: #fff; border: 1.5px solid #94A5F0; border-radius: 12px;
        padding: 10px 14px; box-sizing: border-box; }
.kind { font-size: 11px; font-weight: 600; color: #3B52D9; text-transform: uppercase; }
.title { font-weight: 700; }

Enter fullscreen mode Exit fullscreen mode

The #node-step slot renders every node of type step — declaring the slot is the whole custom-node opt-in. :plugins="true" mounts the minimap, zoom controls and dotted background (lazy-loaded). Give the wrapper a real height: the canvas fills its container, and 100% of zero is a blank page.

2 — Ports and the rules of connection

A workflow editor without connection rules teaches users to build broken flows. Ports carry direction; validators carry your domain:

import { registerConnectionValidator, clearConnectionValidators } from '@grafloria/element';
import { onBeforeUnmount } from 'vue';

const nodes = [
  { id: 'trigger', type: 'step', position: { x: 60, y: 120 }, size: { width: 220, height: 90 },
    data: { title: 'New signup', kind: 'Trigger' },
    ports: [{ id: 't-out', side: 'right', type: 'output' }] },
  { id: 'email', type: 'step', position: { x: 380, y: 120 }, size: { width: 220, height: 90 },
    data: { title: 'Send welcome email', kind: 'Action' },
    ports: [{ id: 'e-in', side: 'left', type: 'input' },
            { id: 'e-out', side: 'right', type: 'output' }] },
];

let dispose;
function onInit(instance) {
  dispose = registerConnectionValidator(({ sourceNode, targetNode }) => {
    if (targetNode?.getData?.('kind') === 'Trigger') return 'a Trigger has no inputs';
    return true;
  });
  instance.getEngine().setInteractionConfig({ portVisibility: 'always' });
}
onBeforeUnmount(() => { dispose?.(); clearConnectionValidators(); });
Enter fullscreen mode Exit fullscreen mode

Wire it with @init="onInit". An input port refuses to start a wire, the validator vetoes with a reason your user sees, and — the part you never have to build — ⌘Z already works: every gesture is one command on the engine's history.

3 — Layout and persistence

// stop hand-placing steps: one prop
//    (or "elk" for gnarlier graphs)

// save — the whole diagram is one document:
import { DiagramSerializer } from '@grafloria/element';
const json = JSON.stringify(new DiagramSerializer().serialize(instance.getModel()));

// text for humans: the same flow as valid Mermaid, positions preserved
const text = instance.exportText();
Enter fullscreen mode Exit fullscreen mode

Where to go from here

The 10-minute Vue tutorial covers the same ground interactively; the Vue deep guides go into slots, state and dashboards; and every demo in the gallery exists as a real Vue SFC with source shown. If you're coming from Vue Flow, the sourced comparison shows exactly where the boxes differ.


Grafloria is an MIT-licensed diagram engine for React, Angular, Vue and plain JavaScript — grafloria.com. If this post was useful, the demo gallery is where the ideas live as running code.

Top comments (0)