Skip to content

Edge relinking - #810

Merged
lukasz-jazwa merged 11 commits into
mainfrom
edge-relinking
Sep 21, 2026
Merged

lukasz-jazwa merged 11 commits into
mainfrom
edge-relinking

Conversation

@Jacek-Synergy

@Jacek-Synergy Jacek-Synergy commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

What

Two opt-in features for existing edges. Both are off by default.

  • Dangling edges (danglingEdges config). An edge can have one or both ends free: empty source/target, with the free end anchored at sourcePosition/targetPosition. With enabled, a draw dropped on empty canvas keeps the edge (shouldKeepOnDrop decides per edge) and a relink drop detaches the end. With detachOnNodeDelete, deleting a node keeps its edges as dangling edges anchored where the port was (shouldDetachOnNodeDelete decides per end; explicit deletes, hidden elements and edges losing both ends are still deleted). Dangling edges route between their anchors, are saved and restored with the model, copy and paste with their node, and box selection picks them by their anchors.
  • Edge relinking (edgeRelinking config). A selected edge shows a handle at each end. Dragging a handle previews the new connection and commits it on drop. The drop is validated by linking.validateConnection with reason: 'relink'. A drop on empty canvas detaches the end when dangling edges allow it. An invalid drop, a drop back on the same port, a click without a drag and Escape leave the edge unchanged. The handles have a hit area of about 12 px on screen at any zoom, so they work with touch.
  • NgDiagramService.startLinkingFromPosition(position) starts a draw from a point on the canvas. Requires danglingEdges.enabled.

New public API: DanglingEdgesConfig, EdgeRelinkingConfig, ConnectionValidationContext, DanglingEndpoint, EdgeEnd, LinkingRelinkContext, events edgeRelinkStarted/edgeRelinkEnded (EdgeRelinkStartedEvent, EdgeRelinkEndedEvent, EdgeRelinkCancelReason), SelectionRemovedEvent.detachedEdges, NgDiagramModelService.detachEdge/attachEdge/getDanglingEndpoints/getNearestDanglingEndpointInRange, utilities isDanglingEdge, hasFreeEndpoint, getDanglingEndpoints, getNearestDanglingEndpointInRange, computeDetachAnchor, RelinkHandleDirective for custom edge templates, theme variables --ngd-relink-handle-size/-fill/-fill-hover/-stroke/-stroke-width (per edge: --edge-relink-handle-*), CSS hooks ng-diagram-edge--dangling and .dangling.

linking.validateConnection is the single validator for drawing, relinking and attachEdge. It receives an optional fifth argument, ConnectionValidationContext, that says which operation asks (draw, relink, attach) and, for relink and attach, which edge and end.

Internals: relinking reuses the linking action state (linking.relink) and the temporary edge, so the existing middlewares, cancelLinking and the Escape flow cover it. ModelLookup keeps a cached list of dangling edges, so virtualization and the endpoint queries stay independent of the number of edges.

Why

Users need to fix a wrong connection without deleting and redrawing the edge, and to keep connections when a node is removed or when the second node does not exist yet. Both were on the roadmap; the roadmap page moves Edge Relinking to the released features.

Backward compatibility

  • Defaults keep the old behavior. With both features off, a canvas drop still discards the drawn edge, deleting a node still deletes its edges, edges show no handles, and copy/paste is unchanged. detachEdge and startLinkingFromPosition are no-ops with a console warning while danglingEdges.enabled is false.
  • EdgeDrawEndedEvent.source: Node → source?: Node. Compile-time break only for apps with strictNullChecks that read event.source without a check. Migration: event.source?.id. At runtime source is undefined only for draws started with startLinkingFromPosition.
  • edgeDrawEnded for a kept canvas drop reports success: true with the created edge and no target/targetPort. This happens only with danglingEdges.enabled. Apps that created their own edge on reason === 'noTarget' must remove that code before enabling the flag, or edges are created twice. The deprecated edgeDrawn never fires for kept dangling edges.
  • linking.validateConnection gets an optional fifth argument. Existing implementations keep working. source can now be null at runtime: for draws started from a position and when the source end of a relinked edge is free. Implementations that assume a non-null source need a guard once they enable these features.
  • SelectionRemovedEvent.detachedEdges: Edge[] is required and always present ([] when nothing was detached). Breaks only code that builds this event object by hand (tests, mocks).
  • FlowConfig.danglingEdges and FlowConfig.edgeRelinking are required. NgDiagramConfig is DeepPartial<FlowConfig>, so app configs are not affected. Only code that builds a complete FlowConfig literal must add both keys.
  • ModelActionType gains 'startRelinking', 'finishRelinking' and 'startLinkingFromPosition'. Breaks middlewares with an exhaustive switch (never check) or a Record<ModelActionType, …>.
  • NgDiagramBaseEdgeComponent gets the public signals dangling, relinkHandlesVisible, relinkSourceHandle, relinkTargetHandle and relinkHandleHitRadius (all @since 1.4.0). Subclasses that declare members with these names conflict. The component injects NgDiagramService and RendererService as optional, so TestBeds that mount it without provideNgDiagram() keep working.
  • Z-index after a programmatic endpoint change. updateEdge or attachEdge changing source/target now recomputes the edge's z-index for its new nodes. Before, the edge kept the layering computed for its old nodes.
  • Models that already contain edges with empty source/target are affected by the routing, box selection and paste changes: such edges now route between their stored positions, are selected by a box around their anchors, and paste moves their free ends. Edges with both ends connected behave as before.
  • No public API was removed or deprecated. The API report is regenerated from the development build.

Docs

New guides guides/edges/dangling-edges and guides/edges/relinking. Updates in the edges overview (host class .dangling), intro/services, guides/flow-config, intro/roadmap and the CHANGELOG under Unreleased.

Jacek-Synergy and others added 11 commits September 14, 2026 07:31
Conflict resolution:
- deleteNodes keeps main's descendant cascade (#806) and feeds the cascaded
  set into partitionIncidentEdges for detach-on-delete.
- copy-paste takes main's structure (#807: resolvePastedEnd,
  collectFreeEdgeEndpointPositions, calculateCenter, empty-paste early exit)
  and re-applies from the branch: manual-routing points shifted with a free
  pasted end, and the dangling-aware isInsideCopiedSet copy rule gated on
  danglingEdges.enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukasz-jazwa
lukasz-jazwa marked this pull request as ready for review September 21, 2026 22:12
@lukasz-jazwa
lukasz-jazwa merged commit 418ba5e into main Sep 21, 2026
2 checks passed
@lukasz-jazwa
lukasz-jazwa deleted the edge-relinking branch September 21, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants