Skip to content
D
Documentation

Ports and validation

concept
3 min readUpdated

Ports determine where a link attaches; validation determines whether the proposed connection is allowed. The framework bindings describe ports, the engine evaluates port-local rules, and the renderer draws the result.

mermaid
flowchart LR
  S[Source port] --> P{"Port rules"}
  P -->|direction, type, capacity| V[Connection validators]
  V -->|every validator passes| L[Link is offered]
  V -->|any validator vetoes| R[Link is refused]

Default ports

If a node declares no ports, Grafloria gives it four deterministic bidirectional ports: top, right, bottom, and left. Their ids are <nodeId>__top, <nodeId>__right, <nodeId>__bottom, and <nodeId>__left. An edge without handles can therefore choose the side facing its partner as the nodes move. A side name such as bottom pins the connection to that default port.

Declared ports

Use a PortSpec when a node needs named endpoints or stricter connection rules. The spec is declared on the node:

ts
import type { PortSpec } from '@grafloria/renderer';

const transformPorts: PortSpec[] = [
    { id: 'in', side: 'left', type: 'input' },
    { id: 'out', side: 'right', type: 'output', dataType: 'frame' },
    {
      id: 'errors',
      side: 'bottom',
      type: 'output',
      maxConnections: 1,
      shape: { shape: 'diamond', size: 12 },
      label: { text: 'errors', layout: 'outside' },
    },
];

side and index place ports along a node edge. type is input, output, or bi. shape changes the glyph, label adds text, and layout controls the port arrangement. maxConnections is the legacy total cap; gating is the richer form for directional connectability, directional caps, allowed types, self-links, and duplicate links. fromSpot, toSpot, and spread control link attachment and the distribution of several links on one edge.

The live DiagramInstance returned by render() owns the mounted view. The instance reconciles these specs into PortModel objects, while the engine keeps the port state used for connection checks.

Three layers of connection checks

Direction and capacity

Port-local rules reject an input used as a wire's source and reject an output used as its target. A full maxConnections port rejects another link. Directional gates add isConnectableStart and isConnectableEnd, while fromMaxLinks and toMaxLinks cap outgoing and incoming links separately. During a drag, an invalid target is marked as invalid by the interaction state.

Type compatibility

Set dataType on ports that carry a named data-flow type. Grafloria checks the two declared data types for compatibility and uses the type when rendering the port glyph. An allowedTypes whitelist provides a port-local restriction based on the other port's type identity: dataType, then systemType, then direction.

Custom rules

Use registerConnectionValidator for a graph rule that needs both endpoint nodes or ports. A ConnectionValidator receives one candidate containing sourceNode, sourcePort, targetNode, targetPort, and an optional existing link during reconnection. Return true to allow the candidate, false to veto it, or a string to veto it with a reason.

All registered validators must pass. They have veto power, not voting power: one veto is enough to refuse the connection. A throwing validator is also treated as a veto.

The registry is process-global rather than per canvas. Keep the returned disposer and run it when the view unmounts. clearConnectionValidators removes every registered validator, so use it only when the view owns the registry rather than sharing it with another mounted diagram.

A mounted typed diagram with a veto rule

This complete browser entry mounts two nodes with visible, typed ports. The rendered result shows a number output feeding a number input, while the validator refuses any candidate whose target node is sink. The returned instance remains the handle for the mounted diagram; unmount() demonstrates cleanup without removing the diagram during setup.

ts
import { render } from '@grafloria/element';
import {
  clearConnectionValidators,
  registerConnectionValidator,
} from '@grafloria/renderer';
import type { ConnectionValidator } from '@grafloria/renderer';

document.body.innerHTML = '<div id="diagram" style="height: 400px"></div>';
const container = document.getElementById('diagram');
if (!container) {
  throw new Error('Diagram container was not found');
}

const validator: ConnectionValidator = ({ targetNode }) => {
  return targetNode.id === 'sink' ? 'The sink cannot receive a link' : true;
};

const removeValidator = registerConnectionValidator(validator);
const instance = render(
  {
    nodes: [
      {
        id: 'source',
        position: { x: 40, y: 120 },
        size: { width: 140, height: 80 },
        label: 'Source',
        ports: [
          { id: 'source-out', side: 'right', type: 'output', dataType: 'number' },
        ],
      },
      {
        id: 'sink',
        position: { x: 300, y: 120 },
        size: { width: 140, height: 80 },
        label: 'Sink',
        ports: [
          { id: 'sink-in', side: 'left', type: 'input', dataType: 'number' },
        ],
      },
    ],
    edges: [
      {
        id: 'number-flow',
        source: 'source',
        target: 'sink',
        sourceHandle: 'source-out',
        targetHandle: 'sink-in',
      },
    ],
  },
  container,
);

function unmount(): void {
  removeValidator();
  clearConnectionValidators();
  instance.dispose();
}
The mounted diagram shows the Source and Sink nodes with their typed ports and connecting wire.

The nodes and their typed ports render inside the 400-pixel-high container. A connection candidate with source-out and sink-in also satisfies the port directions and matching dataType; the custom validator still vetoes it because its target node is sink.

Where next

Was this page helpful?