Skip to Content
ExamplesEdges

Temporary Edges

In Vue Flow, almost everything is built around the concepts of nodes and edges. Edges are the connections between nodes, but what if we want to create an edge that is only connected to one node? What about an “edge” not connected to any nodes at all?

This example shows how to create an “incomplete” edge when a user releases a connection line without making a connection. A ghost node is rendered where the connection line was released, and a temporary edge is added to the flow. Making use of editable edges, the user can pick the edge back up and complete the connection at which point the ghost node is removed!

Instead of connecting A directly to B in the flow, try releasing the connection line in an empty space to create a temporary edge!

import type { ConnectEndEvent, Connection, Edge, EdgeChangeset, EdgeReconnectEndEvent, EdgeReconnectEvent } from '@xyflow/vue'; import { nextTick } from 'vue'; import { useVueFlow } from '@xyflow/vue'; export function useIncompleteEdge() { const { addEdges, addNodes, removeEdges, removeNodes, reconnectEdge, screenToFlowPosition, getNodes, getEdges } = useVueFlow(); let ghostId = 0; let isReconnecting = false; function onConnect(connection: Connection) { addEdges([connection]); } function onReconnectStart() { isReconnecting = true; } // a fresh connection dropped in empty space spawns a ghost node and a temporary edge to it. // `connectEnd` also fires while reconnecting, so skip the spawn in that case. function onConnectEnd({ event, connectionState }: ConnectEndEvent) { if (isReconnecting || connectionState.isValid || connectionState.fromHandle?.type === 'target' || !connectionState.fromNode) { return; } const point = 'changedTouches' in event ? event.changedTouches[0] : event; const position = screenToFlowPosition({ x: point.clientX, y: point.clientY }); const fromNodeId = connectionState.fromNode.id; const id = `ghost-${++ghostId}`; addNodes([{ id, type: 'ghost', position, data: {} }]); addEdges([{ id: `${fromNodeId}->${id}`, source: fromNodeId, target: id, reconnectable: 'target' }]); } function onReconnect(event: EdgeReconnectEvent) { reconnectEdge(event.edge, event.connection); } // dropping a temporary edge's reconnectable (target) end in empty space removes it; either way the // now-orphaned ghost node is pruned function onReconnectEnd(event: EdgeReconnectEndEvent) { if (event.handleType !== 'source') { return; } if (!event.connectionState.isValid && event.edge) { removeEdges([event.edge.id]); } isReconnecting = false; nextTick(pruneGhostNodes); } // there's no `edgesDelete` event, so prune ghost nodes whenever an edge is removed function onEdgesChange(changes: EdgeChangeset<Edge>) { if (changes.getByType('remove').length > 0) { nextTick(pruneGhostNodes); } } function pruneGhostNodes() { const connected = new Set<string>(); for (const edge of getEdges.value) { connected.add(edge.source); connected.add(edge.target); } const orphans = getNodes.value.filter((node) => node.type === 'ghost' && !connected.has(node.id)).map((node) => node.id); if (orphans.length) { removeNodes(orphans); } } return { connect: onConnect, connectEnd: onConnectEnd, reconnectStart: onReconnectStart, reconnect: onReconnect, reconnectEnd: onReconnectEnd, edgesChange: onEdgesChange, }; }
Last updated on