# Validate connections

Refuse connections that break an application rule, and give users a reason for each refusal.

Use a registered validator for rules that depend on your diagram's data, such as preventing a Sink from acting as a connection source. The validator evaluates a proposed connection on the live canvas; framework bindings still use the same model and rule.

## Mount the diagram and register a rule

The examples mount a small Source → Transform → Sink diagram. A registered rule rejects any proposal whose source node has `data.role === 'sink'`; returning a string supplies the refusal reason, while `true` allows the proposal. The callback can also update the small status line so the reason appears in the view.

For typed graph data and mounting the canvas in JavaScript, Angular, Qwik, React, or Vue, see [Build runnable workflows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows). This page adds the connection rule: register [`registerConnectionValidator`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-ext-functions#registerconnectionvalidator) when the view mounts and keep its returned disposer for teardown.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';
import { registerConnectionValidator } from '@grafloria/renderer';

const host = document.createElement('div');
host.style.height = '500px';
host.style.width = '100%';
document.body.append(host);

const status = document.createElement('p');
status.textContent = 'Try drawing a connection out of the Sink.';
document.body.insertBefore(status, host);

const nodes = [
  { id: 'source', position: { x: 80, y: 120 }, size: { width: 120, height: 46 }, label: 'Source', data: { role: 'source' } },
  { id: 'transform', position: { x: 320, y: 120 }, size: { width: 120, height: 46 }, label: 'Transform', data: { role: 'transform' } },
  { id: 'sink', position: { x: 560, y: 120 }, size: { width: 120, height: 46 }, label: 'Sink', data: { role: 'sink' } },
];
const edges = [];

const dispose = registerConnectionValidator(({ sourceNode }) => {
  if (sourceNode.data?.role === 'sink') {
    status.textContent = 'Refused: a Sink has no outputs';
    return 'A Sink has no outputs';
  }
  return true;
});

const instance = render({ nodes, edges }, host);
instance.fitView();
window.addEventListener('pagehide', dispose, { once: true });
```
```ts title="Angular"
import { Component, OnDestroy, signal } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
import { registerConnectionValidator } from '@grafloria/renderer';

@Component({
  selector: 'app-validated-flow',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <p>{{ reason() }}</p>
    <grafloria-diagram-canvas
      [(nodes)]="nodes"
      [(edges)]="edges"
      style="display: block; height: 500px"
    />
  `,
})
export class ValidatedFlowComponent implements OnDestroy {
  readonly reason = signal('Try drawing a connection out of the Sink.');
  nodes: NodeSpec[] = [
    { id: 'source', position: { x: 80, y: 120 }, size: { width: 120, height: 46 }, label: 'Source', data: { role: 'source' } },
    { id: 'transform', position: { x: 320, y: 120 }, size: { width: 120, height: 46 }, label: 'Transform', data: { role: 'transform' } },
    { id: 'sink', position: { x: 560, y: 120 }, size: { width: 120, height: 46 }, label: 'Sink', data: { role: 'sink' } },
  ];
  edges: EdgeSpec[] = [];

  private readonly dispose = registerConnectionValidator(({ sourceNode }) => {
    if (sourceNode.data['role'] === 'sink') {
      this.reason.set('Refused: a Sink has no outputs');
      return 'A Sink has no outputs';
    }
    return true;
  });

  ngOnDestroy(): void {
    this.dispose();
  }
}
```
```tsx title="Qwik"
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
import { render } from '@grafloria/element';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
import { registerConnectionValidator } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'source', position: { x: 80, y: 120 }, size: { width: 120, height: 46 }, label: 'Source', data: { role: 'source' } },
  { id: 'transform', position: { x: 320, y: 120 }, size: { width: 120, height: 46 }, label: 'Transform', data: { role: 'transform' } },
  { id: 'sink', position: { x: 560, y: 120 }, size: { width: 120, height: 46 }, label: 'Sink', data: { role: 'sink' } },
];
const edges: EdgeSpec[] = [];

export default component$(() => {
  const host = useSignal<HTMLElement>();
  const reason = useSignal('Try drawing a connection out of the Sink.');

  useVisibleTask$(({ cleanup }) => {
    const container = host.value;
    if (!container) return;

    const dispose = registerConnectionValidator(({ sourceNode }) => {
      if (sourceNode.data['role'] === 'sink') {
        reason.value = 'Refused: a Sink has no outputs';
        return 'A Sink has no outputs';
      }
      return true;
    });
    const instance = render({ nodes, edges }, container);
    instance.fitView();
    cleanup(() => {
      dispose();
      instance.dispose();
    });
  }, { strategy: 'document-ready' });

  return (
    <div style={{ height: '500px' }}>
      <p>{reason.value}</p>
      <div ref={host} style={{ height: '460px' }} />
    </div>
  );
});
```
```tsx title="React"
import { useEffect, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
import { registerConnectionValidator } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'source', position: { x: 80, y: 120 }, size: { width: 120, height: 46 }, label: 'Source', data: { role: 'source' } },
  { id: 'transform', position: { x: 320, y: 120 }, size: { width: 120, height: 46 }, label: 'Transform', data: { role: 'transform' } },
  { id: 'sink', position: { x: 560, y: 120 }, size: { width: 120, height: 46 }, label: 'Sink', data: { role: 'sink' } },
];
const edges: EdgeSpec[] = [];

export default function ValidatedFlow() {
  const [reason, setReason] = useState('Try drawing a connection out of the Sink.');

  useEffect(() => {
    const dispose = registerConnectionValidator(({ sourceNode }) => {
      if (sourceNode.data?.role === 'sink') {
        setReason('Refused: a Sink has no outputs');
        return 'A Sink has no outputs';
      }
      return true;
    });
    return dispose;
  }, []);

  return (
    <div style={{ height: '500px' }}>
      <p>{reason}</p>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
    </div>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { EdgeSpec, NodeSpec } from '@grafloria/vue';
import { registerConnectionValidator } from '@grafloria/renderer';

const nodes: NodeSpec[] = [
  { id: 'source', position: { x: 80, y: 120 }, size: { width: 120, height: 46 }, label: 'Source', data: { role: 'source' } },
  { id: 'transform', position: { x: 320, y: 120 }, size: { width: 120, height: 46 }, label: 'Transform', data: { role: 'transform' } },
  { id: 'sink', position: { x: 560, y: 120 }, size: { width: 120, height: 46 }, label: 'Sink', data: { role: 'sink' } },
];
const edges: EdgeSpec[] = [];
const reason = ref('Try drawing a connection out of the Sink.');
let dispose: ReturnType<typeof registerConnectionValidator> | undefined;

onMounted(() => {
  dispose = registerConnectionValidator(({ sourceNode }) => {
    if (sourceNode.data?.role === 'sink') {
      reason.value = 'Refused: a Sink has no outputs';
      return 'A Sink has no outputs';
    }
    return true;
  });
});

onBeforeUnmount(() => dispose?.());
</script>

<template>
  <div style="height: 500px">
    <p>{{ reason }}</p>
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" />
  </div>
</template>
```
:::

When the canvas mounts, it shows Source, Transform, and Sink with no links. Drag from Source to Transform or Transform to Sink to make a legal connection; dragging out of Sink leaves no link and changes the status line to “Refused: a Sink has no outputs.” The validator's returned string is the rule's reason; the status line is application UI updated by the same callback.

## Validator contract

Write the rule against the source and target nodes. Return `true` to allow the proposal, `false` to veto without a reason, or a string to veto with that reason. Every registered validator must pass, so a single veto refuses the connection.

| Return value | Type | Effect |
| --- | --- | --- |
| Allow | `true` | Accepts this validator's candidate. |
| Veto | `false` | Refuses without a reason string. |
| Veto with reason | `string` | Refuses and supplies the returned text as the reason. |

## Pitfalls

- The validator registry is process-global, not per canvas. Keep the disposer returned by `registerConnectionValidator` and call it when this view unmounts; otherwise its rule also applies to other diagrams in the same process.
- [`clearConnectionValidators`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-ext-functions#clearconnectionvalidators) removes every registered validator, not only the current view's. Use it only when you intentionally want a clean registry; normal teardown calls the view's own disposer.
- Port direction and type compatibility are separate built-in validation layers. Use [Configure ports](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/configure-ports) for rules expressed by port direction or data types; register a custom validator for application-specific rules.

## See it running

Open the [live connection-validation demo](https://grafloria.com/demos/interaction/validation.html) to try a rejected Sink connection and accepted connections in the browser. Its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/interaction/validation.html) drives the connection pipeline with a registered validator.

## Related

- [Configure ports](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/configure-ports) for direction and data-type constraints.
- [Handle connection interactions](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/handle-connection-interactions) for responding to connection gestures.
