Use interaction settings to choose how users connect nodes, reconnect endpoints and bend wires without writing pointer handlers. In the React example pictured below, A connects to B and C sits below B: reconnect the wire to C, switch to bend editing, or copy and duplicate a selected node. The engine owns these edits; framework bindings expose the mounted canvas and its change events.
1. Define the editing policy
In your browser application, create this shared file. The data uses the library's NodeSpec and EdgeSpec types. The policy checks its fields against InteractionConfig.
DiagramEngine exposes the commands used by the buttons. copy(), paste() and duplicate() return Promise<void>, not new specs. Read the edited graph through the mounted canvas, or use the binding's change events.
tsimport type { DiagramEngine, InteractionConfig } from '@grafloria/engine';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
export const nodes: NodeSpec[] = [
{ id: 'a', label: 'A', position: { x: 80, y: 160 }, size: { width: 120, height: 60 } },
{ id: 'b', label: 'B', position: { x: 480, y: 80 }, size: { width: 120, height: 60 } },
{ id: 'c', label: 'C', position: { x: 480, y: 300 }, size: { width: 120, height: 60 } },
];
export const edges: EdgeSpec[] = [
{ id: 'ab', source: 'a', target: 'b', sourceHandle: 'right',
targetHandle: 'left', type: 'direct' },
];
export const interaction = {
dragThreshold: 8,
enableLinkReconnection: true,
showLinkEndpointHandles: true,
enableWaypointEditing: false,
showWaypointHandles: true,
enableHelperLines: true,
enableKeyboardNudge: true,
} satisfies Partial<InteractionConfig>;
export const actions = ['Reconnect', 'Edit bends', 'Copy', 'Paste', 'Duplicate', 'Lock / unlock'] as const;
export type EditingAction = typeof actions[number];
export async function act(engine: DiagramEngine, action: EditingAction): Promise<void> {
switch (action) {
case 'Reconnect':
engine.setInteractionConfig({ enableWaypointEditing: false });
break;
case 'Edit bends':
engine.setInteractionConfig({ enableWaypointEditing: true, showWaypointHandles: true });
break;
case 'Copy':
if (engine.getDiagram()?.getSelectedNodes().length) {
await engine.copy();
}
break;
case 'Paste':
if (engine.hasClipboardData()) {
await engine.paste({ offset: { x: 30, y: 30 }, selectPasted: true });
}
break;
case 'Duplicate':
if (engine.getDiagram()?.getSelectedNodes().length) {
await engine.duplicate({ offset: { x: 30, y: 30 }, selectDuplicated: true });
}
break;
case 'Lock / unlock': {
const diagram = engine.getDiagram();
if (diagram) diagram.setReadonly(!diagram.isReadonly());
break;
}
}
}
The initial policy enables alignment helper lines and arrow-key nudging. Reconnection starts enabled; bend editing starts disabled. Copy a selected node, then paste to add and select an offset copy. Copy does nothing in this toolbar when no node is selected; the engine's copy command requires a selected node. Duplicate performs the copy-like operation without requiring an earlier Copy click. The clipboard belongs to the engine.
2. Mount it in your framework
Choose one implementation below. Each uses the same shared file and displays the same six controls. Select a node before Copy or Duplicate. The selection count shows the current selection, not keyboard focus.
JavaScript / TypeScript
Start with the mounted canvas in JavaScript quick start, then use the shared editing policy above.
React
bashnpm install @grafloria/react @grafloria/engine @grafloria/renderer @grafloria/element react react-dom
This GrafloriaFlow example adds the shared interaction policy, an 8 px dragThreshold and editing actions on the mounted DiagramInstance; see Validate port connections for initialization and selection-callback wiring.
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges, interaction, actions, act, type EditingAction } from './editing';
export default function Editor() {
const instance = useRef<DiagramInstance | null>(null);
const [selected, setSelected] = useState(0);
async function run(action: EditingAction) {
const current = instance.current;
if (!current) return;
await act(current.getEngine(), action);
current.renderNow();
}
return <>
<div>{actions.map(action => <button key={action} onClick={() => void run(action)}>{action}</button>)}</div>
<p aria-live="polite">Selected: {selected}</p>
<div style={{ height: 440 }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
interaction={interaction} dragThreshold={8}
onInit={current => { instance.current = current; }}
onSelectionChange={change => setSelected(change.nodes.length + change.edges.length)} />
</div>
</>;
}
Vue
bashnpm install @grafloria/vue @grafloria/engine @grafloria/renderer @grafloria/element vue
Render GrafloriaFlow, retain its instance with @init, and read selection through @selection-change.
Known issue: Vue has no
dragThresholdprop, and the shared DOM binder captures its threshold at creation instead of reading the engine's live setting. The intendedinteraction: { dragThreshold: 8 }does not change Vue's mouse threshold. Until the binding forwards it, keep the default 4 px threshold in Vue; the other settings below still apply.
vue<script setup lang="ts"> import { shallowRef, ref } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance } from '@grafloria/renderer'; import { nodes, edges, interaction, actions, act, type EditingAction } from './editing'; const instance = shallowRef<DiagramInstance | null>(null); const selected = ref(0); function init(current: DiagramInstance) { instance.value = current; current.getEngine().setInteractionConfig({ ...interaction, dragThreshold: 4 }); } async function run(action: EditingAction) { const current = instance.value; if (!current) return; await act(current.getEngine(), action); current.renderNow(); } </script> <template> <div><button v-for="action in actions" :key="action" @click="run(action)">{{ action }}</button></div> <p aria-live="polite">Selected: {{ selected }}</p> <div style="height: 440px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :interaction="{ ...interaction, dragThreshold: 4 }" @init="init" @selection-change="change => selected = change.nodes.length + change.edges.length" /> </div> </template>
Vue renders the same initial diagram and controls pictured in the React section.
Qwik
bashnpm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
Pass the shared editing policy and an 8 px dragThreshold to GrafloriaFlow; see Validate port connections for the binding's initialization and selection events. Keep the live instance out of serialized state with noSerialize().
tsximport { component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { DiagramInstance } from '@grafloria/renderer';
import { nodes, edges, interaction, actions, act } from './editing';
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
const selected = useSignal(0);
return <>
<div>{actions.map(action => (
<button key={action} onClick$={async () => {
const current = instance.value;
if (!current) return;
await act(current.getEngine(), action);
current.renderNow();
}}>{action}</button>
))}</div>
<p aria-live="polite">Selected: {selected.value}</p>
<div style={{ height: '440px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
interaction={interaction} dragThreshold={8}
onInit$={current => { instance.value = noSerialize(current); }}
onSelectionChange$={change => {
selected.value = change.nodes.length + change.edges.length;
}} />
</div>
</>;
});
Qwik renders the same initial diagram and six controls pictured in the React section. Use those controls to switch between reconnection and bend editing, copy, paste, duplicate or toggle the document lock.
Angular
bashnpm install @grafloria/angular @grafloria/engine @grafloria/renderer @grafloria/element @angular/common @angular/core @angular/forms @angular/platform-browser rxjs
Render DiagramCanvasComponent with two-way specs. Read its mounted engine through activeEngine(). Angular exposes snapping and keyboard navigation as inputs; enable them directly. Use the SelectionChange output payload to update the count.
tsimport { AfterViewInit, Component, signal, viewChild } from '@angular/core';
import { DiagramCanvasComponent, type SelectionChange } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, interaction, actions, act, type EditingAction } from './editing';
@Component({
selector: 'app-editor',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<div>@for (action of actions; track action) {
<button (click)="run(action)">{{ action }}</button>
}</div>
<p aria-live="polite">Selected: {{ selected() }}</p>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[enableSnapping]="true" [enableKeyboardNavigation]="true"
[enableProximityConnect]="false" (selectionChange)="selectionChanged($event)"
style="display: block; height: 440px" />
`,
})
export class EditorComponent implements AfterViewInit {
readonly canvas = viewChild.required(DiagramCanvasComponent);
readonly actions = actions;
readonly selected = signal(0);
nodes: NodeSpec[] = nodes;
edges: EdgeSpec[] = edges;
ngAfterViewInit(): void {
this.canvas().activeEngine()?.setInteractionConfig(interaction);
}
selectionChanged(change: SelectionChange): void {
this.selected.set(change.nodes.length + change.edges.length);
}
async run(action: EditingAction): Promise<void> {
const engine = this.canvas().activeEngine();
if (!engine) return;
await act(engine, action);
this.canvas().scheduleRender();
}
}
Angular renders the same initial diagram and controls pictured in the React section.
3. Try connections, reconnections and bends
- Hover a node to reveal its ports. Drag a port to a compatible target port to create a connection.
- Click the A → B wire to select it. Drag its endpoint handle at B onto C. The existing link now connects A to C.
- Click Edit bends. Click the selected wire's body to insert a waypoint, then drag that waypoint. The route bends through the point while its endpoints stay attached.
- Click Reconnect before dragging an endpoint again.
Waypoints are model constraints, not independent painted handles. They persist with the edge and participate in undo; see Route and label edges for routing around those constraints.
Known issue: With waypoint editing enabled, a path hit can insert a waypoint before the endpoint-reconnection branch runs. The intended combined policy is
setInteractionConfig({ enableWaypointEditing: true, enableLinkReconnection: true }); until the hit-order conflict is fixed, disable waypoint editing while reconnecting. The two mode buttons above do that without rebuilding the canvas.
Options that matter
Defaults below are engine defaults unless a surface is named explicitly.
| Option | Type | Default | What it does |
|---|---|---|---|
dragThreshold | number | 4 | Separates a pointer click from a drag in screen pixels. JavaScript, React and Qwik also expose a creation-level option/prop. |
enableLinkReconnection | boolean | true | Allows moving an existing link endpoint. |
showLinkEndpointHandles | boolean | true | Displays endpoint handles on selected links. |
enableWaypointEditing | boolean | false | Enables inserting, moving and removing bends. |
showWaypointHandles | boolean | true | Displays handles on selected links. It does not enable editing by itself. |
snapToPortRadius | number | 30 | Sets the connection's port-snap radius. |
enableHelperLines | boolean | false | Snaps a single top-level node to sibling alignments/equal spacing and displays dashed guides during dragging. |
enableKeyboardNudge | boolean | false | Enables selected-node nudging in the shared DOM binding. |
enableSnapping (Angular) | boolean | true | Enables the canvas's snapping layer. |
enableKeyboardNavigation (Angular) | boolean | true | Enables focus navigation, nudging, keyboard connection and announcements. |
readonly (JavaScript/React/Vue/Qwik) | boolean | false in the DOM binder | Prevents editing gestures while retaining viewing interaction. |
For grid snapping, set waypointEditor.snapToGrid and waypointEditor.gridSize in the interaction policy. The shipped snapping layer reads the same grid settings as the waypoint editor. Preserve the other editor fields when changing this nested object: setInteractionConfig() merges only the top level. Use getInteractionConfig().waypointEditor as the starting point. Angular additionally exposes canvasBounds for keeping dragged boxes inside a rectangle.
Keyboard, touch and read-only operation
In the React and Vue examples, click a node, then press an arrow to nudge it by one world unit; Shift increases the step to ten. The policy explicitly enables nudging: it is not enabled by the engine default. Ctrl/⌘+A selects all; Ctrl/⌘+C copies, Ctrl/⌘+V pastes, and Ctrl/⌘+D duplicates selected nodes. Ctrl/⌘+Z undoes, and Ctrl/⌘+Shift+Z or Ctrl/⌘+Y redoes. Text-entry targets keep their keys.
Angular's enabled keyboard navigation also draws a focus ring. Tab and Shift+Tab walk nodes and links; arrows nudge a selection or move focus spatially when nothing is selected. On a focused node, C starts keyboard connection, arrows choose a port, Tab chooses a target and Enter commits. Escape cancels that keyboard connection. Focus is distinct from selection.
Touch uses the shipped gesture handling: one finger taps to select, drags a node to move it, drags from a port to connect, or drags empty canvas to pan. Two fingers pan and pinch to zoom. A 500 ms long press emits a context-menu event; it does not create your menu UI. The binding sets touch-action: none; do not override it with native browser scrolling on the canvas. Touch uses its own 10 px movement tolerance, rather than the mouse's dragThreshold.
Lock / unlock toggles the live document's setReadonly() state. Selection and viewing remain available, but document writes are refused. For a canvas that starts view-only, pass readonly: true to render() or readonly to the React, Vue or Qwik component. Angular has no readonly input on this canvas: set the mounted document lock through its engine, as the shared action does. Keep viewing controls separate with enablePan and the surface's zoom switch (enableZoom, or Angular's enableMouseWheelZoom).
Use Delete or Backspace to remove the selection through the canvas's keyboard handler. Commands and history explains the undo stack those edits join.
Known issue:
engine.deleteSelection()checks the store's selection sets, whereas a shared DOM-binding mouse selection lives on the diagram. The intended call can throw “No entities selected” after a visible selection. Until the helper synchronizes those sources, use the canvas's Delete/Backspace handler, which reads the diagram selection and executes removal commands. Angular's keyboard handler synchronizes selection before calling the helper.
Live demos and related tasks
- Reconnect an edge: select the wire and move its target from B to C.
- Edit waypoints: insert and drag bends.
- Keyboard and screen-reader operation: explore focus, connections and announcements.
- Validate port connections: control which targets accept a wire.
- Add editor controls: add shipped toolbars and canvas controls.
- State and event flow: mirror edits outside the canvas.
Was this page helpful?