Dangling Edges
since v1.4.0Dangling edges are edges with one or both endpoints not connected to any node. They are useful when connections are sketched before both nodes exist, when deleting a node should keep its connections, or when free-floating lines (for example annotations) are part of your diagram.
The feature is fully opt-in. With the default settings, an edge dropped on empty canvas is discarded, and deleting a node deletes its edges.
Try it below: draw an edge from a port onto empty canvas to keep it as a dangling edge, select a node and press Delete to detach its edges instead of deleting them, or select the connected edge and drag one of its handles onto empty canvas to detach that end.
Model Shape
Section titled “Model Shape”A free (unconnected) endpoint has an empty string in source or target, and its anchor position is stored in sourcePosition or targetPosition:
model = initializeModel({5 collapsed lines
nodes: [ { id: 'node-1', position: { x: 100, y: 100 }, data: { label: 'Node 1' } }, { id: 'node-2', position: { x: 500, y: 100 }, data: { label: 'Node 2' } }, { id: 'node-3', position: { x: 500, y: 300 }, data: { label: 'Node 3' } }, ], edges: [8 collapsed lines
{ id: 'connected', source: 'node-1', sourcePort: 'port-right', target: 'node-2', targetPort: 'port-left', data: {}, }, { id: 'dangling', source: '', // free endpoint: no node, no port sourcePosition: { x: 200, y: 340 }, // anchored here, in flow coordinates target: 'node-3', targetPort: 'port-left', data: {}, }, ], });A free endpoint never has a port: when an endpoint is detached, its sourcePort or targetPort is cleared. An edge with both endpoints free is a dual dangling edge. It routes between its two anchor positions.
Enabling the Feature
Section titled “Enabling the Feature”Dangling edges are controlled by the danglingEdges configuration:
config = {6 collapsed lines
zoom: { zoomToFit: { onInit: true, padding: 80, }, }, danglingEdges: { // Keep edges dropped on empty canvas enabled: true, // Deleting a node detaches its edges instead of deleting them detachOnNodeDelete: true, }, // Optional: selected edges get handles, so users can also detach an end by dragging it onto empty canvas linking: { defaultRelinkable: true, }, } satisfies NgDiagramConfig;enabled is the master switch. When true:
- An edge drawn onto empty canvas is kept as a dangling edge instead of being discarded.
- An edge relink dropped on empty canvas detaches that endpoint.
detachEdgeandstartLinkingFromPositionbecome available. With the feature off, they do nothing and log a console warning.
Keeping Edges Dropped on Empty Canvas
Section titled “Keeping Edges Dropped on Empty Canvas”With enabled: true, every edge draw released on empty canvas is kept as a dangling edge, anchored where the pointer was released. A drop on a port that the edge cannot connect to (for example a port with the wrong direction) does not count as a drop on empty canvas, so the draw is discarded. To decide per edge, provide shouldKeepOnDrop:
const config: NgDiagramConfig = { danglingEdges: { enabled: true, // Keep only edges drawn from "output" ports as dangling shouldKeepOnDrop: (edge, dropPosition) => edge.sourcePort?.startsWith('output') ?? false, },};The edge argument is the final edge, after linking.finalEdgeDataBuilder has run. Returning false discards the edge, which is also what happens when the feature is off. The same callback runs for a relink dropped on empty canvas. There it receives the edge as it would be after the detach, and returning false reverts the relink.
Detaching Edges on Node Delete
Section titled “Detaching Edges on Node Delete”By default, deleting a node deletes all of its edges. Set detachOnNodeDelete to keep them as dangling edges instead, anchored where their port was:
const config: NgDiagramConfig = { danglingEdges: { enabled: true, detachOnNodeDelete: true, },};For per-edge control, provide shouldDetachOnNodeDelete. It is called once for each endpoint that loses its node, and returning false deletes the edge:
const config: NgDiagramConfig = { danglingEdges: { enabled: true, detachOnNodeDelete: true, shouldDetachOnNodeDelete: (edge, deletedNode, end) => edge.data?.keepOnDelete === true, },};The detach rules are:
- An explicit delete always wins. An edge that is itself part of the deleted selection is deleted, never detached.
- Hidden wiring never becomes visible. An edge that is hidden only because of the node it loses (for example the edges of the collapsed children of a deleted group) is deleted. Detaching it would turn invisible wiring into a visible dangling edge. An edge that is hidden for another reason (its own
hiddenflag, a template binding, or a hidden node at the other end) is detached like any other edge and stays hidden. - An edge that loses both endpoints in one delete is deleted by default. It becomes a dual dangling edge only when
shouldDetachOnNodeDeleteis defined and returnstruefor both ends. - Deleting a node with the Delete key follows the same rules. The detached edges are reported in the
selectionRemovedevent (see Events below).
Dual Dangling Edges
Section titled “Dual Dangling Edges”An edge with both endpoints free is fully supported:
- Routing — the edge routes between its two anchor positions with any routing algorithm, like a connected edge.
- Persistence —
toJSON()saves thesourcePosition/targetPositionof free endpoints, andinitializeModelrestores them, so dangling edges survive a save and load cycle. - Box selection — a free endpoint counts by its anchor position, so a box drawn around a dangling end selects the edge.
Drawing Edges from a Position
Section titled “Drawing Edges from a Position”NgDiagramService.startLinkingFromPosition starts drawing an edge from a point on the canvas instead of from a node, for example from a context menu action:
private ngDiagramService = inject(NgDiagramService);private viewportService = inject(NgDiagramViewportService);
startDrawingAt(event: MouseEvent) { const position = this.viewportService.clientToFlowPosition({ x: event.clientX, y: event.clientY }); this.ngDiagramService.startLinkingFromPosition(position);}The free end of the edge follows the pointer until a click finishes the draw. A click on a port connects the edge to that port. A click on empty canvas leaves both ends free (a dual dangling edge), and the edge is kept when shouldKeepOnDrop allows it. An edge drawn from a position has no source node, so it is a dangling edge from the start. For that reason this method requires danglingEdges.enabled; with the feature off, the call is ignored and a console warning is logged. For draws started this way, linking.validateConnection receives null as the source node.
Utilities and Service Methods
Section titled “Utilities and Service Methods”The package exports these helper functions. Import them from ng-diagram like any other symbol:
import { hasFreeEndpoint, isDanglingEdge } from 'ng-diagram';
private modelService = inject(NgDiagramModelService);
readonly danglingEdges = computed(() => this.modelService.edges().filter(isDanglingEdge));
isSourceFree(edge: Edge): boolean { return hasFreeEndpoint(edge, 'source');}isDanglingEdge(edge)— whether the edge has at least one free endpoint.hasFreeEndpoint(edge, end?)— whether the given endpoint (or either, whenendis omitted) is free.getDanglingEndpoints(edges)— collects the free endpoints of the given edges asDanglingEndpointentries (a dual dangling edge gives two).getNearestDanglingEndpointInRange(edges, point, range)— the free endpoint nearest to a point within a range.computeDetachAnchor(edge, end, node)— the position where a detached endpoint stays: the port’s current position, otherwise the edge’s routed endpoint, or the node’s center as a last resort.
NgDiagramModelService provides the model-level operations:
getDanglingEndpoints()andgetNearestDanglingEndpointInRange(point, range)— the same queries, run over all edges in the model.detachEdge(edgeId, end, position?)— detaches one endpoint into a free endpoint. Withoutposition, the endpoint stays anchored where it is now. RequiresdanglingEdges.enabled; with the feature off, it does nothing and logs a console warning.attachEdge(edgeId, end, nodeId, portId?)— the opposite operation: connects a free endpoint to a node and, optionally, a port. It applies the same checks as a relink drop (the node must exist and be visible, the port must exist and have the right direction) and runslinking.validateConnectionwithreason: 'attach'. It returns whether the connection was valid and applied.
private modelService = inject(NgDiagramModelService);
async reconnectLooseEnds() { for (const { edge, end, position } of this.modelService.getDanglingEndpoints()) { const port = this.modelService.getNearestPortInRange(position, 50); if (port) { await this.modelService.attachEdge(edge.id, end, port.nodeId, port.id); } }}Styling
Section titled “Styling”Dangling edges get dedicated CSS classes, so you can style them without checking the model:
- The edge wrapper element gets the
ng-diagram-edge--danglingclass. Temporary draw previews do not get it, even though they always have a free end. ng-diagram-base-edgegets thedanglinghost class, and itspathcarries thedanglingclass as well.
// Dashed rendering for any edge with a free endpointng-diagram-base-edge.dangling { --edge-stroke-dasharray: 6 4; --edge-stroke-opacity: 0.7;}Events
Section titled “Events”edgeDrawEnded— a canvas drop kept as a dangling edge reportssuccess: truewith the creatededge, but notarget/targetPort. To tell a connected draw from a kept dangling draw, checkevent.target. The deprecatededgeDrawnevent does not fire for dangling edges; it only reports node-to-node connections.selectionRemoved— thedetachedEdgesfield lists the edges that were detached instead of deleted. The snapshots are taken after the detach, so the freed endpoints already have an emptysource/targetand their positions set. The field is always present and is[]when nothing was detached.
onEdgeDrawEnded(event: EdgeDrawEndedEvent) { if (event.success && !event.target) { console.log('Kept as dangling edge:', event.edge!.id); }}