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:

FieldWhat it does
side, indexwhich edge of the node, and ordering along it
typeinput / output / bi — directionality, enforced while wiring
dataTypea named data-flow type; drives the glyph color and compatibility (below)
shapeglyph: circle (default), square, diamond, triangle, or a custom SVG path
labeltext + placement: inside / outside / orthogonal / radial
group + metadata.portGroupsshared config and a layout strategy — sideLinear, line, ellipseSpread — for many-port nodes
maxConnections, gatinglink caps; gating is the richer form (directional connectability, allowed types, self-links)
fromSpot / toSpot, spreadwhere links leave the glyph box, and fanning several links along an edge
Ports show on hover by default. To show them always: 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.

The registry is process-global, not per-canvas. Always keep the returned disposer and call it (plus clearConnectionValidators()) when your view unmounts — otherwise validators leak across routes, remounts, and React StrictMode's double-invoke.

Live: validation demos →

Where next

  • The model — ports as engine objects (PortModel).
  • The tutorials wire typed ports end-to-end in each framework.