Validate connections during a drag
Use a connection validator when your graph has rules that port direction alone does not express. The validator runs before the link is created: return true to allow the candidate, or return a string to veto it with a reason. In this example, an output may connect to an input, but never to another output.
Install
JavaScript uses the element, renderer, and engine packages:
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine
Install the binding package as well when you use Angular, Qwik, React, or Vue.
The render function mounts the graph and returns its live DiagramInstance. Use the clearConnectionValidators function before replacing a process-wide rule.
Register the rule
The connection validator receives the candidate's source and target ports. Keep the returned disposer and remove the process-wide registration when the view unmounts.
The following rule allows an incomplete candidate, then rejects output-to-output connections with a readable reason:
tsimport { registerConnectionValidator } from '@grafloria/renderer';
const disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (!sourcePort || !targetPort) return true;
if (sourcePort.type === 'output' && targetPort.type === 'output') {
return 'an output cannot feed another output';
}
return true;
});
The disposer belongs in your component's unmount or effect cleanup, not immediately after setup. Registered validators are process-global, so leaving one active can change the rules on another canvas.
Mount a graph
Give the host a resolved height. Hover over a node to reveal its ports, then drag A (out) to C (out): the wire disappears and no edge is created. Drag it to B (in): the connection remains on the canvas.
The Qwik binding uses GrafloriaFlow, and its onInit$ callback receives the shared DiagramInstance. The other bindings expose the same instance through their own init callback.
jsimport { render } from '@grafloria/element';
import { registerConnectionValidator, clearConnectionValidators } from '@grafloria/renderer';
const nodes = [
{
id: 'a', position: { x: 120, y: 260 }, size: { width: 120, height: 70 }, label: 'A (out)',
ports: [{ id: 'ao', side: 'right', type: 'output' }],
},
{
id: 'b', position: { x: 640, y: 140 }, size: { width: 120, height: 70 }, label: 'B (in)',
ports: [{ id: 'bi', side: 'left', type: 'input' }],
},
{
id: 'c', position: { x: 640, y: 400 }, size: { width: 120, height: 70 }, label: 'C (out)',
ports: [{ id: 'co', side: 'left', type: 'output' }],
},
];
const host = document.createElement('div');
host.id = 'canvas';
document.body.append(host);
host.style.height = '100vh';
const disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (!sourcePort || !targetPort) return true;
if (sourcePort.type === 'output' && targetPort.type === 'output') {
return 'an output cannot feed another output';
}
return true;
});
const instance = render({ nodes, edges: [] }, host);
instance.renderNow();
window.addEventListener('pagehide', () => {
disposeValidator();
clearConnectionValidators();
instance.dispose();
}, { once: true });
tsimport { AfterViewInit, Component, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { clearConnectionValidators, registerConnectionValidator } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:100vh" />
`,
})
export class ConnectionValidationComponent implements AfterViewInit, OnDestroy {
canvas = viewChild.required(DiagramCanvasComponent);
nodes = [
{ id: 'a', position: { x: 120, y: 260 }, size: { width: 120, height: 70 }, label: 'A (out)', ports: [{ id: 'ao', side: 'right' as const, type: 'output' }] },
{ id: 'b', position: { x: 640, y: 140 }, size: { width: 120, height: 70 }, label: 'B (in)', ports: [{ id: 'bi', side: 'left' as const, type: 'input' }] },
{ id: 'c', position: { x: 640, y: 400 }, size: { width: 120, height: 70 }, label: 'C (out)', ports: [{ id: 'co', side: 'left' as const, type: 'output' }] },
];
edges = [];
private disposeValidator?: () => void;
ngAfterViewInit(): void {
clearConnectionValidators();
this.disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (!sourcePort || !targetPort) return true;
return sourcePort.type === 'output' && targetPort.type === 'output'
? 'an output cannot feed another output'
: true;
});
}
ngOnDestroy(): void {
this.disposeValidator?.();
clearConnectionValidators();
}
}
tsximport { component$, $, useVisibleTask$ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { clearConnectionValidators, registerConnectionValidator } from '@grafloria/element';
const nodes = [
{ id: 'a', position: { x: 120, y: 260 }, size: { width: 120, height: 70 }, label: 'A (out)', ports: [{ id: 'ao', side: 'right' as const, type: 'output' as const }] },
{ id: 'b', position: { x: 640, y: 140 }, size: { width: 120, height: 70 }, label: 'B (in)', ports: [{ id: 'bi', side: 'left' as const, type: 'input' as const }] },
{ id: 'c', position: { x: 640, y: 400 }, size: { width: 120, height: 70 }, label: 'C (out)', ports: [{ id: 'co', side: 'left' as const, type: 'output' as const }] },
];
export default component$(() => {
useVisibleTask$(({ cleanup }) => {
clearConnectionValidators();
const disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (!sourcePort || !targetPort) return true;
return sourcePort.type === 'output' && targetPort.type === 'output'
? 'an output cannot feed another output'
: true;
});
cleanup(() => { disposeValidator(); clearConnectionValidators(); });
});
return (
<div style={{ height: '100vh' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={[]} onInit$={$((instance: DiagramInstance) => {
instance.renderNow();
})} />
</div>
);
});
tsximport { useEffect } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { clearConnectionValidators, registerConnectionValidator } from '@grafloria/element';
const nodes = [
{ id: 'a', position: { x: 120, y: 260 }, size: { width: 120, height: 70 }, label: 'A (out)', ports: [{ id: 'ao', side: 'right' as const, type: 'output' as const }] },
{ id: 'b', position: { x: 640, y: 140 }, size: { width: 120, height: 70 }, label: 'B (in)', ports: [{ id: 'bi', side: 'left' as const, type: 'input' as const }] },
{ id: 'c', position: { x: 640, y: 400 }, size: { width: 120, height: 70 }, label: 'C (out)', ports: [{ id: 'co', side: 'left' as const, type: 'output' as const }] },
];
export default function ConnectionValidation() {
useEffect(() => {
clearConnectionValidators();
const disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (!sourcePort || !targetPort) return true;
return sourcePort.type === 'output' && targetPort.type === 'output'
? 'an output cannot feed another output'
: true;
});
return () => { disposeValidator(); clearConnectionValidators(); };
}, []);
const onInit = (instance: DiagramInstance): void => {
instance.renderNow();
};
return <div style={{ height: '100vh' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={[]} onInit={onInit} /></div>;
}
vue<script setup lang="ts"> import { onBeforeUnmount } from 'vue'; import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue'; import { clearConnectionValidators, registerConnectionValidator } from '@grafloria/element'; const nodes = [ { id: 'a', position: { x: 120, y: 260 }, size: { width: 120, height: 70 }, label: 'A (out)', ports: [{ id: 'ao', side: 'right' as const, type: 'output' as const }] }, { id: 'b', position: { x: 640, y: 140 }, size: { width: 120, height: 70 }, label: 'B (in)', ports: [{ id: 'bi', side: 'left' as const, type: 'input' as const }] }, { id: 'c', position: { x: 640, y: 400 }, size: { width: 120, height: 70 }, label: 'C (out)', ports: [{ id: 'co', side: 'left' as const, type: 'output' as const }] }, ]; let disposeValidator: (() => void) | undefined; function onInit(instance: DiagramInstance): void { clearConnectionValidators(); disposeValidator = registerConnectionValidator(({ sourcePort, targetPort }) => { if (!sourcePort || !targetPort) return true; return sourcePort.type === 'output' && targetPort.type === 'output' ? 'an output cannot feed another output' : true; }); instance.renderNow(); } onBeforeUnmount(() => { disposeValidator?.(); clearConnectionValidators(); }); </script> <template> <div style="height:100vh"><GrafloriaFlow :default-nodes="nodes" :default-edges="[]" @init="onInit" /></div> </template>
Each binding mounts the same model and registers the same rule. During a drag, a rejected drop leaves the model unchanged; an allowed drop adds the link to the mounted diagram. Press Escape during a drag to cancel the attempt.
Options that affect the interaction
| Option | Type | Default | What it does |
|---|---|---|---|
interaction.portVisibility | 'hover' | 'always' | 'hover' | Keeps ports visible during the example when set to 'always'. |
ports[].type | 'input' | 'output' | 'bi' | — | Controls which direction a port accepts during wiring. |
Pitfalls
- Validators are process-global. Dispose the returned function and call
clearConnectionValidators()when the view unmounts; see ports and validation for the warning. - Give the canvas host a real height, or the mounted diagram appears blank. See style a diagram.
- Every registered validator must allow the candidate. Validators have veto power, not voting power.
Live demo
Try the connection-validation demo. It rejects output-to-output and accepts output-to-input during real drags.
Was this page helpful?