Observe a connection drag from start through completion or cancellation, and let users create links by dragging from a node body to another node.
Use body-to-body connection when users should not have to aim at a small port. The built-in mode starts from the nearest port on the first node and completes at the nearest port on the second. With the mode off, the usual interaction is port-to-port dragging, and dragging a node body moves the node.
Mount the diagram and listen to its connection lifecycle
These examples mount two typed nodes and an empty edge list, then enable body-to-body connection and write each lifecycle event to a visible log. Drag anywhere on the source node to the target node and release to create a link; the log records the gesture as it happens. Drag off the target or press Escape to cancel instead.
For the JavaScript, Angular, and Vue mounting patterns and access to the live canvas, see Build runnable workflows. This page adds body-to-body connection and a log of its lifecycle events. The diagram data uses NodeSpec and EdgeSpec.
jsimport { render } from '@grafloria/element';
const log = document.createElement('pre');
log.style.height = '88px';
log.style.overflow = 'auto';
log.textContent = 'Drag from source to target to see the connection lifecycle.';
const canvas = document.createElement('div');
canvas.style.width = '700px';
canvas.style.height = '420px';
document.body.append(log, canvas);
/** @type {import('@grafloria/element').NodeSpec[]} */
const nodes = [
{
id: 'src',
position: { x: 80, y: 70 },
size: { width: 150, height: 60 },
label: 'source',
ports: [{ id: 'src.out', side: 'right', type: 'output' }],
},
{
id: 'dst',
position: { x: 430, y: 70 },
size: { width: 150, height: 60 },
label: 'target',
ports: [{ id: 'dst.in', side: 'left', type: 'input' }],
},
];
/** @type {import('@grafloria/element').EdgeSpec[]} */
const edges = [];
const instance = render({ nodes, edges }, canvas);
const engine = instance.getEngine();
engine.setInteractionConfig({ enableEasyConnect: true });
const connectionEvents = [
'connection:start',
'connection:update',
'connection:port-enter',
'connection:port-leave',
'connection:complete',
'connection:cancel',
];
for (const name of connectionEvents) {
engine.eventBus.on(name, () => {
if (log.textContent?.startsWith('Drag from')) log.textContent = '';
log.prepend(`${name}\n`);
});
}
tsimport { AfterViewInit, Component, OnDestroy, signal, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div style="height: 88px; overflow: auto">
@for (row of rows(); track $index) {
<div>{{ row }}</div>
}
</div>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display: block; width: 700px; height: 420px" />
`,
})
export class ConnectionInteractionsComponent implements AfterViewInit, OnDestroy {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly rows = signal<string[]>(['Drag from source to target to see the connection lifecycle.']);
nodes: NodeSpec[] = [
{
id: 'src',
position: { x: 80, y: 70 },
size: { width: 150, height: 60 },
label: 'source',
ports: [{ id: 'src.out', side: 'right', type: 'output' }],
},
{
id: 'dst',
position: { x: 430, y: 70 },
size: { width: 150, height: 60 },
label: 'target',
ports: [{ id: 'dst.in', side: 'left', type: 'input' }],
},
];
edges: EdgeSpec[] = [];
private disposers: Array<() => void> = [];
ngAfterViewInit(): void {
const engine = this.canvas().activeEngine();
if (!engine) return;
engine.setInteractionConfig({ enableEasyConnect: true });
const connectionEvents = [
'connection:start',
'connection:update',
'connection:port-enter',
'connection:port-leave',
'connection:complete',
'connection:cancel',
];
this.disposers = connectionEvents.map((name) =>
engine.eventBus.on(name, () => this.rows.update((rows) => [name, ...rows].slice(0, 12))),
);
}
ngOnDestroy(): void {
this.disposers.forEach((dispose) => dispose());
}
}
vue<script setup lang="ts"> import { onBeforeUnmount, ref } from 'vue'; import { GrafloriaFlow, type DiagramInstance, type EdgeSpec, type NodeSpec } from '@grafloria/vue'; const nodes: NodeSpec[] = [ { id: 'src', position: { x: 80, y: 70 }, size: { width: 150, height: 60 }, label: 'source', ports: [{ id: 'src.out', side: 'right', type: 'output' }], }, { id: 'dst', position: { x: 430, y: 70 }, size: { width: 150, height: 60 }, label: 'target', ports: [{ id: 'dst.in', side: 'left', type: 'input' }], }, ]; const edges: EdgeSpec[] = []; const connectionEvents = [ 'connection:start', 'connection:update', 'connection:port-enter', 'connection:port-leave', 'connection:complete', 'connection:cancel', ]; const rows = ref<string[]>(['Drag from source to target to see the connection lifecycle.']); let disposers: Array<() => void> = []; function onInit(instance: DiagramInstance): void { const engine = instance.getEngine(); engine.setInteractionConfig({ enableEasyConnect: true }); disposers = connectionEvents.map((name) => engine.eventBus.on(name, () => { rows.value = [name, ...rows.value].slice(0, 12); }), ); } onBeforeUnmount(() => disposers.forEach((dispose) => dispose())); </script> <template> <div style="width: 700px; height: 520px"> <div style="height: 88px; overflow: auto"> <div v-for="(row, index) in rows" :key="`${index}-${row}`">{{ row }}</div> </div> <div style="height: 420px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" @init="onInit" /> </div> </div> </template>
The log names the six engine events. connection:start carries the source port and valid target ports; connection:update reports the current target and whether it is valid, including a rejection reason; connection:port-enter and connection:port-leave mark target-port crossings; connection:complete identifies the connected ports; and connection:cancel marks an abandoned or refused drag. update can fire repeatedly while the pointer moves. A successful drop ends with complete; a refused drop or Escape ends with cancel.
Open the live connection-events demo or read its source.
Choose the connection gesture
| Option | Type | Default | What it does |
|---|---|---|---|
enableEasyConnect | boolean | false | Starts a connection from the nearest port when the user presses a node body, instead of moving the node. |
JavaScript and Vue turn on enableEasyConnect after mount with instance.getEngine().setInteractionConfig(...). Angular accesses its engine through activeEngine() and calls setInteractionConfig(...) there. Leave the option off to keep body-drag-to-move and start connections from ports instead. With it on, press anywhere on the source node and release on the target node; the built-in interaction creates the link without a custom tool.
Open the live easy-connect-body demo or read its source.
Watch for
- Keep the event subscriptions for the lifetime of the mounted canvas and call each returned disposer when the host unmounts. Otherwise, repeated mounts can leave old listeners recording the same gesture.
enableEasyConnectchanges what a node-body press does: it starts a connection rather than moving that node. Leave the option disabled when users need body dragging to move nodes.- This example logs event names. Read the event payload when the host needs port details or a validation reason.
Was this page helpful?