Skip to content

v1.3.0 - Awaitable API & Gesture Reliability

Latest

Choose a tag to compare

@piotrblaszczyk piotrblaszczyk released this 11 Aug 07:36
29bb7b0

✨ Added

  • NgDiagramDefaultEdgeLabelComponent is now public — the component rendering the default edge label chip (ng-diagram-default-edge-label) is exported from the package, so custom edge templates can compose it inside ng-diagram-base-edge-label and get the default label look (theme-aware chip, hover and selected border highlights) without copying its styles. The hover and selected border highlights now work in any edge template, and the new --edge-label-border-color-hover and --edge-label-border-color-selected variables override their colors (each falling back to --edge-label-border-color, then the matching --ngd-default-edge-stroke-hover/-selected token). (#777)
  • Configurable resize sidesng-diagram-node-resize-adornment accepts an activeSides input that limits which sides of the node can be grabbed, for example [activeSides]="['right', 'bottom']" for a node anchored at its top-left corner. All four lines keep rendering (they double as the selection frame), but the ones left out are inert and show no resize cursor, and a corner handle renders only when both of its sides are listed. Omitting the input keeps all four sides and corners active, so existing templates are unaffected. The new Side type names one of the four sides of a rectangular area; PortSide is now an alias of it (#775)
  • Remove ports from default nodes – this is now possible to remove ports from default nodes (#759)
  • waitForMeasurements option on service methods — addNodes, addEdges, updateNode, updateNodes, updateNodeData, updateEdge, updateEdges, updateEdgeData on NgDiagramModelService, resizeNode on NgDiagramNodeService and paste on NgDiagramClipboardService accept options?: { waitForMeasurements?: boolean }; when set, the returned promise resolves only after the elements affected by the change have been measured — useful whenever the next step depends on real dimensions (for example zoomToFit() or centerOnNode()). The option exists only on methods whose changes can trigger measurements; deletions and other model-only operations have nothing to measure, so awaiting the method itself is already enough there. Inside an already active transaction the option is ignored with a console warning — pass { waitForMeasurements: true } to the transaction itself instead (#769)
  • Customizable runtime-property strippinginitializeModel and initializeModelAdapter accept an optional InitializeModelOptions parameter to control which properties are stripped on initialization and toJSON(). Overriding the defaults can break the diagram — use at your own risk (#760)
  • Resize snap offset — new computeSnapOffsetForNodeSize and defaultResizeSnapOffset options on SnappingConfig. Snapped node sizes now follow the sequence offset + n * snap per axis, so a node with a 60px header and a 50px vertical resize snap can snap to 60, 110, 160, … instead of 50, 100, 150, …. Defaults to { width: 0, height: 0 } (#765, #770) — thanks @logan-brd for the suggestion! 🙏
  • NgDiagramService.transaction always returns the commit promise — the synchronous-callback overload used to return void, and for some async callbacks the commit promise was silently discarded; all overloads now return Promise<TransactionResult>, so awaiting a transaction reliably waits for its commit (#769)
  • Cancel in-progress gestures – new NgDiagramService.cancelActiveInteraction() aborts the active linking, drag, resize, rotate or pan gesture immediately and restores the pre-gesture state: dragged nodes snap back to their initial positions, resized/rotated nodes regain their original geometry, and the temporary edge is discarded (state cleared, document listeners removed, no need to wait for pointer release). Bound to Escape by default via the new cancelInteraction shortcut action. The edgeDrawEnded event gains a cancelled reason, and nodeDragEnded/nodeResizeEnded/nodeRotateEnded gain an optional cancelReason field (#747, #766)

🧩 Changed

  • NgDiagramService.invalidateMeasurements is awaitable — it returns a Promise<void> that resolves once the triggered re-measurements have settled, so the invalidated elements' size, measuredPorts and measuredLabels read fresh on the next line. An element that delivers no new measurement (unmounted, zero-size, or unchanged) does not stall the promise. Existing call sites keep working (voidPromise<void>); ignoring the promise is fine (#778)
  • Awaitable service methods — every mutating method on the public services (NgDiagramModelService, NgDiagramNodeService, NgDiagramSelectionService, NgDiagramClipboardService, NgDiagramGroupsService, NgDiagramViewportService) now returns a Promise<void> that resolves once the change has been applied to the model, so the next line of code reads the updated state. Read it through getModel() or the getter methods (getNodeById, getEdgeById, getConnectedEdges, …) — these are synchronous with the model. The nodes(), edges() and metadata() signals refresh with Angular's change detection, so right after the await (and inside diagram event handlers) they can still show the previous state. Existing call sites keep working (voidPromise<void>). Do not await these promises from inside a middleware — the update pipeline is not re-entrant and the await would deadlock; fire-and-forget calls are safe there (#769)

🐛 Fixed

  • Gesture reliability — drag, resize, rotate and linking now behave correctly with slow (asynchronous) middlewares and throwing user callbacks: nodeDragStarted/nodeDragEnded, nodeResizeStarted/nodeResizeEnded and nodeRotateStarted/nodeRotateEnded always come in pairs and report the node(s) of their own gesture; a throwing user callback (e.g. connection validation or a grouping check) no longer leaves a gesture stuck — linking cannot become permanently blocked and node measurements are not suppressed afterwards; and deleting a node mid-gesture (while resizing, rotating or linking it) cleans up correctly (#769)
  • A throwing middleware no longer freezes the diagram — an uncaught error (or unhandled rejection) inside a middleware used to silently freeze every subsequent update; it now rejects the promise returned by the mutating call and the diagram keeps working (#769)
  • waitForMeasurements no longer resolves early when measurement rounds overlap — a leftover debounce timer from a previous round could settle a newly started round before its measurements arrived (#778)
  • Initialization resilience — an error thrown during initialization (e.g. by a custom ModelAdapter) no longer leaves the diagram in a state where nothing gets measured; initialization completes and the error is logged (#769)
  • waitForMeasurements accuracy — a concurrent unrelated update can no longer make waitForMeasurements resolve too early or wait for the wrong elements; the option passed to a nested transaction now logs a warning instead of silently resolving before the updates are applied. As a consequence, a transaction now waits only for measurements caused by its own changes: a transaction with an empty callback resolves immediately, so it no longer works as a barrier that waits for unrelated measurement activity (a pattern some apps used after invalidateMeasurements) — await invalidateMeasurements() itself instead (#769)
  • Overlapping transactions no longer lose updates — a transaction (or a plain service call) issued while a previous un-awaited transaction was still committing used to be silently dropped; every transaction now applies its own updates, and the overlap logs a console warning recommending to await transactions (#769)
  • Port and edge-label measurements no longer get lost during rapid re-rendering — fast mount/unmount cycles (e.g. virtualization while panning) could apply port and label updates out of order or drop them entirely when one update failed, leaving stale port positions or labels; a failed batch no longer blocks all future ones either (#769)
  • NgDiagramNodeService.resizeNode now resizes an unselected group — a programmatic resizeNode call targeting a group node used to silently do nothing unless the group was selected; it now applies regardless of selection state, still enforcing the group resize constraints (children containment, minimum node size and resize snapping). Interactive resize behavior is unchanged. Calls that used to silently no-op now apply (#772)
  • Dangling edges survive persistenceinitializeModel and initializeModelAdapter no longer strip the authored sourcePosition/targetPosition of an edge's free endpoint (empty source/target), and toJSON() now includes them in the serialized output, so dangling edges load from a persisted model the same way they work when added at runtime; no more "Invalid edge coordinates detected" for valid dangling edges on init (#751, #760)
  • Port side/type no longer stay stale after a port moves — recreating a port with the same id in a different place (e.g. toggling a port between a side: 'left' and a side: 'right' block) now updates measuredPorts with the new side/type, so edges anchor to the correct side; measured size/position keep coming from the DOM as before. The same applies to edge labels re-registered with a changed positionOnEdge (#750, #763)
  • Group with children jumping on resize snap — resizing a group that contains child nodes from the bottom/right edge no longer moves the group when a resize snap is configured (#765, #770) — thanks @logan-brd for the issue submission! 🙏
  • Keyboard shortcuts work when a gesture starts with focus outside the diagram — the resize/rotate handles stop the pointerdown propagation, which used to skip the diagram's focus grab; starting a resize right after clicking an external control (e.g. a toolbar button) left every shortcut dead — in particular Escape could not cancel the gesture. The diagram now takes focus on any pointerdown inside it (#766)
  • Touch gestures stay exclusive under virtualization — on touch devices with virtualization enabled, nodes and ports leaving the rendered area during a pan or pinch-zoom no longer reset the internal gesture-exclusivity state, so a stray touch can no longer start a second gesture (drag, resize, linking) in the middle of an active one (#766)
  • Resize snapping no longer cuts group children — with allowResizeBelowChildrenBounds: false, a snapped group size that would land inside the children bounds now rounds up to the next snap value that still contains the children (#770)
  • Size changes made right after a resize gesture are no longer overwritten by a stale measurement — a middleware reverting an invalid resize on resizeNodeStop, or a nodeResizeEnded handler correcting the size, used to lose when the mouse button was released while still moving; the corrected size now always wins. The resized node is also re-measured once after the gesture, so when CSS (e.g. min-width) keeps the element larger than the resized size, the model picks up the element's real size (#771, #776) — thanks @logan-brd for the report and the reproduction! 🙏
  • Releasing the mouse while still moving no longer loses the end of a resize, drag or rotation — the node used to stop short of where the pointer was released (the faster the release, the bigger the miss; with resize snapping the difference could reach a whole grid step). The release position is now applied as the final update of the gesture, before nodeDragEnded/nodeResizeEnded/nodeRotateEnded fires, so the reported geometry always matches where the pointer stopped (#771, #779)
  • Drawing an edge is cancelled when another touch gesture takes over — on touch devices, a two-finger pan or pinch started in the middle of drawing an edge used to finish the edge at the other finger's position, sometimes connecting it to a target the user never pointed at; the drawing is now cancelled and edgeDrawEnded reports the cancelled reason (#779)