Skip to content
D
Documentation

Validate connections

how-to
3 min readUpdated

Use declarative port types for ordinary data compatibility, then add a custom validator for rules that depend on the nodes or ports themselves. The rendered diagram offers matching targets, refuses vetoed targets before a link is created, and keeps the reason returned by your validator.

Choose the rule

See Ports and connection rules for dataType, PortSpec, portTypeRegistry, compatibleWith, and registerConnectionValidator. This page adds complete typed-port examples and a validator whose returned reason explains why a proposed target is refused.

Validators are process-global. Keep the disposer returned by registerConnectionValidator() and call it when the mounted diagram leaves the page; do not clear validators owned by another diagram.

Build typed ports

The following data gives the diagram one number output, one number input, and one string input. Registering number as compatible only with number makes the number-to-number drop valid and the number-to-string drop invalid. The number glyphs use the registered blue and the string glyph uses the registered purple.

JavaScript

Use render to mount the spec into a real element.

html
<div id="diagram" style="height: 520px"></div>
<script type="module">
  import { render } from '@grafloria/element';
  import { portTypeRegistry } from '@grafloria/engine';

  portTypeRegistry.registerAll([
    { name: 'number', color: '#2563eb', compatibleWith: ['number'] },
    { name: 'string', color: '#9333ea', compatibleWith: ['string'] },
  ]);

  const nodes = [
    { id: 'source', position: { x: 80, y: 190 }, size: { width: 140, height: 70 }, label: 'number source', ports: [{ id: 'out', side: 'right', type: 'output', dataType: 'number' }] },
    { id: 'number', position: { x: 460, y: 100 }, size: { width: 140, height: 70 }, label: 'number input', ports: [{ id: 'in', side: 'left', type: 'input', dataType: 'number' }] },
    { id: 'string', position: { x: 460, y: 280 }, size: { width: 140, height: 70 }, label: 'string input', ports: [{ id: 'in', side: 'left', type: 'input', dataType: 'string' }] },
  ];

  const container = document.getElementById('diagram');
  if (!(container instanceof HTMLElement)) throw new Error('Missing diagram container');
  const instance = render({ nodes, edges: [] }, container);
  instance.renderNow();
</script>

React

tsx
import { GrafloriaFlow } from '@grafloria/react';
import type { NodeInput } from '@grafloria/renderer';
import { portTypeRegistry } from '@grafloria/engine';

portTypeRegistry.registerAll([
  { name: 'number', color: '#2563eb', compatibleWith: ['number'] },
  { name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);

const nodes = [
  { id: 'source', position: { x: 80, y: 190 }, size: { width: 140, height: 70 }, label: 'number source', ports: [{ id: 'out', side: 'right' as const, type: 'output' as const, dataType: 'number' }] },
  { id: 'number', position: { x: 460, y: 100 }, size: { width: 140, height: 70 }, label: 'number input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'number' }] },
  { id: 'string', position: { x: 460, y: 280 }, size: { width: 140, height: 70 }, label: 'string input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'string' }] },
] satisfies NodeInput[];

export function TypedDiagram() {
  return <div style={{ height: 520 }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={[]} /></div>;
}

Vue

vue
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import { portTypeRegistry } from '@grafloria/engine';
import type { NodeInput } from '@grafloria/renderer';

portTypeRegistry.registerAll([
  { name: 'number', color: '#2563eb', compatibleWith: ['number'] },
  { name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);

const nodes = [
  { id: 'source', position: { x: 80, y: 190 }, size: { width: 140, height: 70 }, label: 'number source', ports: [{ id: 'out', side: 'right' as const, type: 'output' as const, dataType: 'number' }] },
  { id: 'number', position: { x: 460, y: 100 }, size: { width: 140, height: 70 }, label: 'number input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'number' }] },
  { id: 'string', position: { x: 460, y: 280 }, size: { width: 140, height: 70 }, label: 'string input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'string' }] },
] satisfies NodeInput[];
</script>

<template><div style="height:520px"><GrafloriaFlow :default-nodes="nodes" :default-edges="[]" /></div></template>

Angular

ts
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { portTypeRegistry } from '@grafloria/engine';
import type { NodeInput } from '@grafloria/renderer';

portTypeRegistry.registerAll([
  { name: 'number', color: '#2563eb', compatibleWith: ['number'] },
  { name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:520px" />`,
})
export class TypedDiagramComponent {
  nodes = [
    { id: 'source', position: { x: 80, y: 190 }, size: { width: 140, height: 70 }, label: 'number source', ports: [{ id: 'out', side: 'right' as const, type: 'output' as const, dataType: 'number' }] },
    { id: 'number', position: { x: 460, y: 100 }, size: { width: 140, height: 70 }, label: 'number input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'number' }] },
    { id: 'string', position: { x: 460, y: 280 }, size: { width: 140, height: 70 }, label: 'string input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const, dataType: 'string' }] },
  ] satisfies NodeInput[];
  edges = [];
}

Qwik

In a Qwik component, import GrafloriaFlow from @grafloria/qwik. Pass the typed nodes and an empty defaultEdges list, register the data types in the browser lifecycle, and call renderNow() from onInit$ after the instance is available. Give the wrapper a height so the diagram has a drawable host.

Add a custom validator

This rule allows output-to-input connections and rejects output-to-output connections. The callback receives a connection candidate, so the same rule works for a newly drawn link and for a link being reconnected. Return the reason string to expose why the proposed target is refused.

tsx
import { useEffect } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import { registerConnectionValidator } from '@grafloria/renderer';
import type { NodeInput } from '@grafloria/renderer';

const nodes = [
  { id: 'source', position: { x: 80, y: 190 }, size: { width: 140, height: 70 }, label: 'source', ports: [{ id: 'out', side: 'right' as const, type: 'output' as const }] },
  { id: 'input', position: { x: 460, y: 100 }, size: { width: 140, height: 70 }, label: 'input', ports: [{ id: 'in', side: 'left' as const, type: 'input' as const }] },
  { id: 'other-output', position: { x: 460, y: 280 }, size: { width: 140, height: 70 }, label: 'other output', ports: [{ id: 'out', side: 'left' as const, type: 'output' as const }] },
] satisfies NodeInput[];

export function ValidatedDiagram() {
  useEffect(() => {
    const disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
      if (sourcePort === null || targetPort === null) return true;
      if (sourcePort.type === 'output' && targetPort.type === 'output') {
        return 'An output cannot feed another output';
      }
      return true;
    });
    return disposeValidator;
  }, []);

  return <div style={{ height: 520 }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={[]} /></div>;
}

Mount that registration with the diagram rather than at module load. In React, return disposeValidator from the effect cleanup. In Vue, call it from onBeforeUnmount. In Angular, call it from ngOnDestroy. In JavaScript, retain the disposer until you remove the diagram. The live connection-validation demo shows an output-to-input wire being accepted and an output-to-output wire being refused.

Options that affect connection legality

OptionTypeDefaultWhat it does
dataTypestring—Gives a port a registered data-flow type. The type drives glyph colour and compatibility.
gating.allowedTypesstring[]unrestrictedRestricts which port data types may attach.
gating.isConnectableStartbooleantrueControls whether a link may start at the port.
gating.isConnectableEndbooleantrueControls whether a link may end at the port.
gating.fromMaxLinksnumber | nullunlimitedCaps outgoing links.
gating.toMaxLinksnumber | nullunlimitedCaps incoming links.
gating.allowSelfLinkbooleanfalseControls whether a node may connect to itself.
gating.allowDuplicateLinksbooleantrueControls whether a second link between the same ordered ports is allowed.

Pitfalls

  • A type registry is process-wide. Register application types during bootstrap and choose names that do not collide with another diagram.
  • A custom validator is also process-global, and every registered validator must pass. Dispose your registration when its owner unmounts; see ports and connection rules for the registry boundary.
  • A validator does not replace declarative port gating. Use PortSpec and its gating fields for direction and link caps, then reserve the validator for rules that need candidate context.

Was this page helpful?

Validate connections — Grafloria · GPT-5.6 Luna