Use an edge's route to express how it travels, then let the mounted diagram keep that route correct as nodes move. This page covers geometry, obstacle avoidance, labels, waypoints, and reconnecting an existing edge in JavaScript, Angular, Qwik, React, and Vue.
Choose the route and shape
For the EdgeSpec fields source, target, type, and router, see layout and routing. This page adds connector styling, labels, fixed waypoints, obstacle avoidance, and endpoint reconnection.
tsimport type { EdgeSpec } from '@grafloria/renderer';
const edges: EdgeSpec[] = [{
id: 'avoiding-edge', source: 'a', target: 'b', router: 'avoid', connector: 'rounded',
label: 'retry', labelPlacement: 'above', waypoints: [{ x: 420, y: 180 }],
}];
Use connector: 'straight' | 'rounded' | 'smooth' | 'bezier' for the connector treatment. A label is editable in the mounted diagram; labelPlacement: 'above' or 'below' places it beside the line, and labelStyle controls its appearance.
Mount an obstacle-avoiding edge
The following data puts a wall between A and B. The edge renders as a right-angled detour and stays outside the wall when the wall moves. Give the host a height: a zero-height host has no canvas to paint.
jsimport { render } from '@grafloria/element';
const nodes = [
{ id: 'a', position: { x: 80, y: 260 }, size: { width: 120, height: 60 }, label: 'A' },
{ id: 'b', position: { x: 700, y: 260 }, size: { width: 120, height: 60 }, label: 'B' },
{ id: 'wall', position: { x: 390, y: 205 }, size: { width: 140, height: 170 }, label: 'obstacle' },
];
const edges = [{ id: 'e1', source: 'a', target: 'b', router: 'avoid', connector: 'rounded' }];
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const api = render({ nodes, edges }, host, { interaction: { enableWaypointEditing: true, showWaypointHandles: true } });
api.renderNow();
tsimport { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { EdgeSpec } from '@grafloria/renderer';
@Component({ standalone: true, imports: [DiagramCanvasComponent], template: `<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:100vh" />` })
export class EdgeGuideComponent {
nodes = [
{ id: 'a', position: { x: 80, y: 260 }, size: { width: 120, height: 60 }, label: 'A' },
{ id: 'b', position: { x: 700, y: 260 }, size: { width: 120, height: 60 }, label: 'B' },
{ id: 'wall', position: { x: 390, y: 205 }, size: { width: 140, height: 170 }, label: 'obstacle' },
];
edges: EdgeSpec[] = [{ id: 'e1', source: 'a', target: 'b', router: 'avoid', connector: 'rounded' }];
}
tsximport { component$ } from '@builder.io/qwik';
import '@grafloria/element';
const nodes = [
{ id: 'a', position: { x: 80, y: 260 }, label: 'A' },
{ id: 'b', position: { x: 700, y: 260 }, label: 'B' },
{ id: 'wall', position: { x: 390, y: 205 }, label: 'obstacle' },
];
const edges = [{ id: 'e1', source: 'a', target: 'b', router: 'avoid', connector: 'rounded' }];
export default component$(() => <div style={{ height: '100vh' }}><grafloria-flow nodes={JSON.stringify(nodes)} edges={JSON.stringify(edges)} /></div>);
tsximport { GrafloriaFlow } from '@grafloria/react';
const nodes = [
{ id: 'a', position: { x: 80, y: 260 }, size: { width: 120, height: 60 }, label: 'A' },
{ id: 'b', position: { x: 700, y: 260 }, size: { width: 120, height: 60 }, label: 'B' },
{ id: 'wall', position: { x: 390, y: 205 }, size: { width: 140, height: 170 }, label: 'obstacle' },
];
const edges = [{ id: 'e1', source: 'a', target: 'b', router: 'avoid', connector: 'rounded' }];
export default function EdgeGuide() { return <div style={{ height: '100vh' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} /></div>; }
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; const nodes = [ { id: 'a', position: { x: 80, y: 260 }, size: { width: 120, height: 60 }, label: 'A' }, { id: 'b', position: { x: 700, y: 260 }, size: { width: 120, height: 60 }, label: 'B' }, { id: 'wall', position: { x: 390, y: 205 }, size: { width: 140, height: 170 }, label: 'obstacle' }, ]; const edges = [{ id: 'e1', source: 'a', target: 'b', router: 'avoid', connector: 'rounded' }]; </script> <template><div style="height:100vh"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" /></div></template>
The JavaScript render call returns the live DiagramInstance. The wrappers mount the same model; moving wall makes the obstacle-avoiding route recalculate. See the edge-routing demo.
Add labels and fixed bends
Put interior points in waypoints when a route must pass through known bends. Points use world coordinates; do not include endpoint coordinates. The route keeps the bends while its endpoints reattach as nodes move.
tsimport type { EdgeSpec } from '@grafloria/renderer';
const labelledEdge: EdgeSpec = {
id: 'labelled', source: 'a', target: 'b', router: 'orthogonal', connector: 'rounded',
waypoints: [{ x: 360, y: 160 }, { x: 620, y: 160 }], label: 'asks for approval',
labelPlacement: 'above', labelStyle: { color: '#5f6b7a', fontSize: 11 },
};
For interactive editing, enable enableWaypointEditing and showWaypointHandles in the interaction configuration. Select an edge, click its path to add one interior waypoint, then drag the handle. Each bend changes the rendered route and is one undoable user edit. See the editable-edge demo.
jsimport { render } from '@grafloria/element';
const nodes = [{ id: 'a', position: { x: 80, y: 180 }, label: 'A' }, { id: 'b', position: { x: 620, y: 180 }, label: 'B' }];
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const editable = render({ nodes, edges: [{ id: 'e1', source: 'a', target: 'b', type: 'direct' }] }, host, { interaction: { enableWaypointEditing: true, showWaypointHandles: true } });
editable.renderNow();
tsimport { AfterViewInit, Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
@Component({ standalone: true, imports: [DiagramCanvasComponent], template: `<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges" style="display:block;height:100vh" />` })
export class EditableEdgeComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes = [{ id: 'a', position: { x: 80, y: 180 }, label: 'A' }, { id: 'b', position: { x: 620, y: 180 }, label: 'B' }];
edges = [{ id: 'e1', source: 'a', target: 'b', type: 'direct' as const }];
ngAfterViewInit(): void { this.canvas().activeEngine()?.setInteractionConfig({ enableWaypointEditing: true, showWaypointHandles: true }); }
}
tsximport { component$ } from '@builder.io/qwik';
import '@grafloria/element';
const nodes = [{ id: 'a', position: { x: 80, y: 180 }, label: 'A' }, { id: 'b', position: { x: 620, y: 180 }, label: 'B' }];
const edges = [{ id: 'e1', source: 'a', target: 'b', type: 'direct' as const }];
export default component$(() => <div style={{ height: '100vh' }}><grafloria-flow nodes={JSON.stringify(nodes)} edges={JSON.stringify(edges)} interaction={JSON.stringify({ enableWaypointEditing: true, showWaypointHandles: true })} /></div>);
tsximport { GrafloriaFlow } from '@grafloria/react';
const nodes = [{ id: 'a', position: { x: 80, y: 180 }, label: 'A' }, { id: 'b', position: { x: 620, y: 180 }, label: 'B' }];
const edges = [{ id: 'e1', source: 'a', target: 'b', type: 'direct' as const }];
export default function EditableEdge() { return <div style={{ height: '100vh' }}><GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} interaction={{ enableWaypointEditing: true, showWaypointHandles: true }} /></div>; }
vue<script setup lang="ts"> import { GrafloriaFlow } from '@grafloria/vue'; const nodes = [{ id: 'a', position: { x: 80, y: 180 }, label: 'A' }, { id: 'b', position: { x: 620, y: 180 }, label: 'B' }]; const edges = [{ id: 'e1', source: 'a', target: 'b', type: 'direct' as const }]; </script> <template><div style="height:100vh"><GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :interaction="{ enableWaypointEditing: true, showWaypointHandles: true }" /></div></template>
Reconnect an existing edge
Pin an endpoint to a side or port when the connection must land there:
tsimport type { EdgeSpec } from '@grafloria/renderer';
const reconnectable: EdgeSpec = {
id: 'e1', source: 'a', target: 'b', sourceHandle: 'right', targetHandle: 'left', type: 'direct',
};
With waypoint editing off, select the edge, press its endpoint handle, drag to another valid port, and release. The model changes from A→B to A→C; validators run for the new target. Subscribe to the instance's reconnect event when application state needs the new endpoint:
tsimport { render } from '@grafloria/element';
const nodes = [
{ id: 'a', position: { x: 80, y: 260 }, label: 'A' },
{ id: 'b', position: { x: 660, y: 110 }, label: 'B' },
{ id: 'c', position: { x: 660, y: 430 }, label: 'C' },
];
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');
host.style.height = '100vh';
const api = render({ nodes, edges: [{ id: 'e1', source: 'a', target: 'b', sourceHandle: 'right', targetHandle: 'left', type: 'direct' }] }, host);
const stop = api.on('reconnect', () => {
const link = api.getModel().getLink('e1');
if (link) console.log(link.targetNodeId);
});
// Call stop() when the host unmounts.
The same gesture is built into the bindings shown here; no custom pointer handler is required. Keep endpoint handles pinned in a test or demo when you need the handle to sit directly on the port. See the reconnect-edge demo.
Pitfalls
typechooses the line shape;routerchooses the path. Set both when you need both guarantees.waypointscontains interior points only. The router supplies the endpoints.- Waypoint editing is opt-in. Without
enableWaypointEditing, clicking the path does not add a bend. - A mounted canvas needs a resolved height. See edit Mermaid diagrams for the canvas sizing warning.
- Reconnecting an endpoint invokes connection validation again. See ports and connection rules.
Was this page helpful?