Skip to main content

Connections

Add edges between nodes with the connections plugin — anchors, routing styles, and the SVG connection layer.

Connections draw edges between nodes. Install the plugin, create edges, and the connection layer renders them as SVG paths with five routing styles.

0 undo / 0 redo

Setup

Install the package when your board needs edges:

bash
pnpm add @lupinum/board-connections

Then install the connections plugin during engine creation and render the SVG layer under BoardRoot:

ts
import { createBoardEngine } from '@lupinum/board-core'
import { connectionsPlugin } from '@lupinum/board-connections'
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'

const engine = createBoardEngine({
  plugins: [connectionsPlugin({ routing: 'bezier' })],
})
vue
<template>
  <BoardRoot :engine="engine" style="height: 100vh">
    <BoardConnectionLayer />
  </BoardRoot>
</template>

Both pieces are required: the plugin owns edge state, and BoardConnectionLayer renders and edits that state.


Creating edges

Create edges after the endpoint nodes exist:

ts
const edge = engine.plugins.connections.createEdge({
  from: sourceNodeId,
  to: targetNodeId,
  label: 'depends on',
  data: {},
})

For a complete minimal scene:

ts
const source = engine.createNode({
  type: 'text',
  x: 80,
  y: 100,
  text: 'Source',
})

const target = engine.createNode({
  type: 'text',
  x: 420,
  y: 100,
  text: 'Target',
})

engine.plugins.connections.createEdge({
  from: source.id,
  to: target.id,
  label: 'depends on',
  data: {},
})

With anchors

Specify where the edge attaches on each node:

ts
engine.plugins.connections.createEdge({
  from: nodeA,
  to: nodeB,
  fromAnchor: { side: 'right', offset: 0.5 }, // middle of right edge
  toAnchor: { side: 'left', offset: 0.5 }, // middle of left edge
  data: {},
})

Anchor offset is 0–1 along the side (0.5 = center). If omitted, the connection layer auto-picks the best side from node geometry, keeps it stable while nodes move, and attaches at the center of that side.

Use non-center offsets for precise manual anchors:

ts
engine.plugins.connections.createEdge({
  from: nodeA,
  to: nodeB,
  fromAnchor: { side: 'bottom', offset: 0.2 },
  toAnchor: { side: 'top', offset: 0.8 },
  data: {},
})

Querying edges

ts
// All edges
const edges = engine.plugins.connections.getEdges()

// Edges from a specific node
const outgoing = engine.plugins.connections.getEdgesFrom(nodeId)

// Edges to a specific node
const incoming = engine.plugins.connections.getEdgesTo(nodeId)

// Edges between two nodes
const between = engine.plugins.connections.getEdgesBetween(nodeA, nodeB)

// A specific edge
const edge = engine.plugins.connections.getEdge(edgeId)

Updating edges

Reconnect or restyle an existing edge with updateEdge():

ts
engine.plugins.connections.updateEdge(edgeId, {
  to: otherNodeId,
  toAnchor: undefined, // clear explicit anchor so the new node side auto-resolves
})

The connection layer uses the same API internally when you drag an endpoint handle to another node.


Deleting edges

ts
engine.plugins.connections.deleteEdge(edgeId)
Edges are also automatically deleted when either endpoint node is removed.

Rendering with BoardConnectionLayer

Render BoardConnectionLayer anywhere under BoardRoot. It uses the board context, teleports its SVG layer to the root element, and applies the camera transform itself:

vue
<script setup lang="ts">
import { BoardRoot } from '@lupinum/vue-board'
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
</script>

<template>
  <BoardRoot :engine="engine" style="height: 100vh">
    <BoardConnectionLayer />
  </BoardRoot>
</template>

Hover a card edge to reveal a filled connection handle, then drag from it to another card to create a new edge. Dropping a new connection on empty space cancels by default; pass createNodeForConnection if your app wants empty-drop creation to create a node and connect it. Hover an edge or select it to reveal draggable endpoint handles for reconnecting existing edges; dropping a reconnect on empty space cancels it.

UI-created edges use automatic endpoints by default. The edge stores no fromAnchor or toAnchor, so the connection layer keeps choosing the best side as nodes move. Dragging an existing endpoint onto a node side locks that endpoint to the exact side offset under the pointer. Select the edge and use the reset anchor actions to clear manual anchors back to auto. Set endpointMode="manual" on BoardConnectionLayer, or connectionsPlugin({ endpointMode: 'manual' }), when newly created edges should also lock to the side points the user chose.

Routing styles

Toggle the demo above to compare routing styles. Set the routing on BoardConnectionLayer:

vue
<BoardConnectionLayer routing="step" />
StylePurpose
bezier (default)Smooth cubic bezier curve. Best for organic-looking graphs.
smooth-stepRounded orthogonal connector. Clean and readable.
stepRight-angle stepped path. Good for pipeline and workflow layouts.
straightDirect straight line. Simple and lightweight.
arcCurved arc route for sketch-style or hand-drawn edge rendering.

Custom edge rendering

Use the edge slot for full control over edge appearance:

vue
<BoardConnectionLayer>
  <template #edge="{ edge, route }">
    <path :d="route.path" stroke="#3b82f6" stroke-width="2" fill="none" />
    <text :x="route.labelPoint.x" :y="route.labelPoint.y" text-anchor="middle">
      {{ edge.label }}
    </text>
  </template>
</BoardConnectionLayer>

Events

ts
engine.on('edge:created', (edge) => {
  console.log('Edge created:', edge.from, '→', edge.to)
})

engine.on('edge:updated', (edge, prev) => {
  console.log('Edge moved from', prev.to, 'to', edge.to)
})

engine.on('edge:deleted', (edgeId) => {
  console.log('Edge deleted:', edgeId)
})