Learn / Ports & validation
Ports & validation
Ports decide where links attach and which connections are legal. The spec is identical in every framework — declared on the node, enforced by the engine, drawn by the renderer.
The default: zero ceremony
Every node is born with four deterministic bi-directional ports — top, right, bottom,
left, with ids <nodeId>__top … __left. Plain flowcharts
need nothing declared: omit handles on an edge and it attaches to whichever side faces
its partner as nodes move. sourceHandle: 'bottom' pins a default port by
side name.
Declared ports
{ id: 'transform', position: { x: 300, y: 120 }, size: { width: 150, height: 90 },
ports: [
{ id: 'in', side: 'left', type: 'input' },
{ id: 'out', side: 'right', type: 'output', dataType: 'frame' },
{ id: 'errs', side: 'bottom', type: 'output', maxConnections: 1,
shape: { shape: 'diamond', size: 12 },
label: { text: 'errors', layout: 'outside' } },
] }
The full spec, field by field:
| Field | What it does |
|---|---|
side, index | which edge of the node, and ordering along it |
type | input / output / bi — directionality, enforced while wiring |
dataType | a named data-flow type; drives the glyph color and compatibility (below) |
shape | glyph: circle (default), square, diamond, triangle, or a custom SVG path |
label | text + placement: inside / outside / orthogonal / radial |
group + metadata.portGroups | shared config and a layout strategy — sideLinear, line, ellipseSpread — for many-port nodes |
maxConnections, gating | link caps; gating is the richer form (directional connectability, allowed types, self-links) |
fromSpot / toSpot, spread | where links leave the glyph box, and fanning several links along an edge |
interaction: { portVisibility: 'always' } at creation (a prop in
React/Vue), or engine.setInteractionConfig({ portVisibility: 'always' })
at runtime.Live: port shapes, labels, groups and layouts →
Three validation layers
1. Port anatomy — free
Directions and caps enforce themselves: an input refuses to start a
wire, a full maxConnections: 1 port refuses a second, and the invalid
target is tinted while you drag.
2. Type compatibility — declarative
import { portTypeRegistry } from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
Ports carrying a dataType get the registered color, and incompatible
pairs refuse the wire mid-drag — the visual language and the rule are one
declaration.
3. Custom rules — a function with veto power
import { registerConnectionValidator, clearConnectionValidators } from '@grafloria/element';
const dispose = registerConnectionValidator(({ sourceNode, sourcePort, targetNode, targetPort, link }) => {
if (sourcePort?.type === 'output' && targetPort?.type === 'output')
return 'an output cannot feed another output'; // string = veto, with a reason
if (sourceNode?.data?.role === 'sink')
return 'a Sink has no outputs';
return true; // true = allow
});
The candidate carries both nodes, both ports, and — when the user is
reconnecting an existing edge rather than drawing a new one — the
link. Every registered validator must pass: it's veto power, not voting.
clearConnectionValidators()) when your
view unmounts — otherwise validators leak across routes, remounts, and React
StrictMode's double-invoke.