# 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:

```bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
```

Install the binding package as well when you use Angular, Qwik, React, or Vue.

The [render](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-core#render) function mounts the graph and returns its live `DiagramInstance`. Use the [clearConnectionValidators](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-ext-functions#clearconnectionvalidators) function before replacing a process-wide rule.

## Register the rule

The [connection validator](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-ext-functions#registerconnectionvalidator) 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:

```ts
import { 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`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-qwik#grafloriaflow), and its `onInit$` callback receives the shared [`DiagramInstance`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance). The other bindings expose the same instance through their own init callback.

:::code-group
```js title="JavaScript"
import { 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 });
```
```ts title="Angular"
import { 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();
  }
}
```
```tsx title="Qwik"
import { 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>
  );
});
```
```tsx title="React"
import { 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 title="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](https://atloria.dev/p/grafloria-h7YM7amryF/developer/ports-and-validation) for the warning.
- Give the canvas host a real height, or the mounted diagram appears blank. See [style a diagram](https://atloria.dev/p/grafloria-h7YM7amryF/developer/style-a-diagram).
- Every registered validator must allow the candidate. Validators have veto power, not voting power.

## Live demo

Try the [connection-validation demo](https://grafloria.com/demos/ports/connection-validation.html). It rejects output-to-output and accepts output-to-input during real drags.

## Related

- [Ports and validation](https://atloria.dev/p/grafloria-h7YM7amryF/developer/ports-and-validation)
- [Build a workflow editor](https://atloria.dev/p/grafloria-h7YM7amryF/developer/build-a-workflow-editor)
- [Edit and route edges](https://atloria.dev/p/grafloria-h7YM7amryF/developer/edit-and-route-edges)
