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
tsximport { 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
tsimport { 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.
tsximport { 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
| Option | Type | Default | What it does |
|---|---|---|---|
dataType | string | — | Gives a port a registered data-flow type. The type drives glyph colour and compatibility. |
gating.allowedTypes | string[] | unrestricted | Restricts which port data types may attach. |
gating.isConnectableStart | boolean | true | Controls whether a link may start at the port. |
gating.isConnectableEnd | boolean | true | Controls whether a link may end at the port. |
gating.fromMaxLinks | number | null | unlimited | Caps outgoing links. |
gating.toMaxLinks | number | null | unlimited | Caps incoming links. |
gating.allowSelfLink | boolean | false | Controls whether a node may connect to itself. |
gating.allowDuplicateLinks | boolean | true | Controls 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
PortSpecand itsgatingfields for direction and link caps, then reserve the validator for rules that need candidate context.
Related
- Ports and connection rules — port anatomy, gating, and compatibility.
- How Grafloria works — the model and engine behind every binding.
Was this page helpful?