feat(flow): add managed state and v19.1 runtime improvements#320
Merged
Conversation
…g the dom every frame The minimap re-measured every node (2x getBoundingClientRect) and re-created every SVG rect on each animation frame of every pan/zoom - the reason the fNodeRenderLimit=1500 bailout existed. Node rects now come from node position and a size cache invalidated by the existing resize signal, live in flow coordinates, and are reused between frames: panning updates one group transform attribute and zero per-node DOM. The viewport calculation drops its per-frame bounding-box measurement the same way. fNodeRenderLimit default raises 1500 -> 10000. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
…ning every node Every pointerdown ran hostElement.contains against ALL registered nodes in up to six preparations (drag, resize, rotate, select, canvas-drag, drop-to-group, create-connection). Node and group hosts already carry data-f-node-id / data-f-group-id, so one closest() walk plus a registry lookup resolves the owner in O(DOM depth); a host-instance check keeps it correct when several flows share the page. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
…per measurement Every connector rect resolution paid a getComputedStyle call just to read border radii that practically never change. Radii are now cached per element (unscaled) and re-read only when the node resize signal fires — the same invalidation the rest of the measurement pipeline already relies on. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
…emovals Mounting a node with K connectors fired K+1 synchronous nodesChanges notifications, each running every raw listener (a11y semantics rebuild over all nodes, layout, minimap hub) — O(N^2) listener work across an initial render. Notifications now coalesce to one per microtask while revisions keep incrementing synchronously. Registry removals are collected and compacted out of the ordered list in one pass on the next read, so a large teardown is O(n) total instead of per-removal indexOf+splice O(n^2). Lookup maps reflect removals immediately and the getAll() array keeps its identity. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
A single node resize/state change replayed the redraw pipeline over every connection in the graph. The store now tracks which nodes went dirty; the resize path narrows the pass to connections touching those nodes, while every other emitter keeps full-redraw semantics. Scoped passes skip the global connected-state reset (setConnected is idempotent now) and escalate back to a full pass whenever a previous sliced/worker pass is still in flight, so an aborted session can never leave connections undrawn. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
… across handlers Every node redraw re-armed its own clearTimeout+setTimeout pair for the connectable-sides recalculation — a multi-node drag churned N timers per pointer event. Nodes now register with one shared debounced scheduler that flushes them together and skips nodes destroyed while queued. OnPointerMove also stops spreading a fresh copy of the pointer difference per handler (hundreds of allocations per event on multi-select drags); handlers treat the point as read-only, so one object per event is enough. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
onPointerDown/prepareDragSequence/onPointerUp were flat walls of mediator calls whose ORDER was the whole priority contract — invisible and easy to break when adding a gesture. Each phase is now a declared claimant list with the contract documented in one place: the first preparation that claims the pointer wins, and finalization order is intentionally independent of preparation order. Behavior is unchanged. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
…State() provideFFlow(withFlowState()) gives applications a data-first workflow: load plain node/connection records once, render state.nodes()/state.connections() with @for, and take the graph back with snapshot() - no gesture handlers. Finished gestures (create/reassign connection, node moves incl. multi-select, drops into groups, external-item drops, delete requests with connection cascade) are forwarded into the store automatically, each as ONE undoable step. The history engine is self-contained (snapshot stacks over immutably updated records - no extra dependency), with undo/redo and canUndo/canRedo signals. EVERY behavior is overridable: subclass FFlowState - any CRUD method, any apply* gesture handler, any protected building block - and install it via withFlowState({ stateClass }); the auto-wiring dispatches through the subclass. injectFlowState<TNode, TConnection>() returns the typed store; connectionFactory/nodeFactory config hooks cover light-touch veto/shaping. The classic event-driven API is untouched; the undo-redo-v2 example is rewritten onto the plugin and its docs page updated. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
… the manual pattern The state plugin gets its own example and page (/examples/state): the same minimal editor, now documented in depth - auto-applied gestures, built-in undo/redo, load()/snapshot() data flow, programmatic editing sharing the same history, factory hooks, and full behavior overriding through an FFlowState subclass. undo-redo-v2 is restored to its original @foblex/mutator version, so both patterns stay demonstrated side by side. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
- Add a first-class groups collection (IFStateGroup, groups() signal, addGroups/updateGroup/removeGroups/getGroup); moves and drop-to-group handle groups, group removal cascades connections and un-parents children. - Add changes() version signal that ticks once per history step, so a single effect can drive autosave/persistence. - Add selectionInHistory config: selection() is always a live signal, and opting in makes a selection change its own undoable step, restored on undo/redo via the flow's select(). - Flatten the record models: drop the data/type wrapper. Records hold only framework fields and the store is generic over your own shape (FFlowState<TNode extends IFStateNode>); external-item drops spread fData onto the node as its own fields. - Expose FFlowBase.select so the controller restores selection through the public contract. - Expand the flow-state example (external palette, group) and rewrite its docs. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
…itignore - Added image support for the Angular Diagram Example. - Included light and dark mode images for better visual representation. - Updated .gitignore to exclude design documentation directory. - Changed isSelfConnectable property to false for f-connector directive. Signed-off-by: Siarhei Huzarevich <shuzarevich@gmail.com>
- Added `dropToGroup` configuration option to control grouping behavior. - Updated `applyDropToGroup` method to respect the `dropToGroup` setting. - Modified `createNodeRecord` to handle parentId based on `dropToGroup`. - Enhanced documentation to clarify the new drop behavior. - Added tests for drop-to-group scenarios, including disabled grouping. Signed-off-by: Siarhei Huzarevich <shuzarevich@gmail.com>
- Added `dropToGroup` configuration option to control grouping behavior. - Updated `applyDropToGroup` method to respect the `dropToGroup` setting. - Modified `createNodeRecord` to handle parentId based on `dropToGroup`. - Enhanced documentation to clarify the new drop behavior. - Added tests for drop-to-group scenarios, including disabled grouping. Signed-off-by: Siarhei Huzarevich <shuzarevich@gmail.com>
A group/node with fAutoSizeToFitChildren resized to fit its children without emitting fNodeSizeChange/fGroupSizeChange, so listeners could not track the auto-fitted size. Emit the measured rect after the fit, guarded against the node's own stored rect so a settled fit stays silent and can't feed a resize loop. Closes #316 Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
The fit ran synchronously in the container's size-change effect, so on a state-driven change (undo re-parenting a child out) it executed before Angular propagated [fNodeParentId] to the child directive, counted the removed child and re-shrank the container - undo could not restore its size. Defer only the fit dispatch to afterNextRender (same change-detection cycle) for fAutoSizeToFitChildren nodes; the rest stays sync. Closes #317 Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
redraw() set width/height only when _size was defined; clearing the size (input back to undefined, e.g. undo restoring an auto-fit node to its content size) left the stale dimensions on the element. Remove them when _size is falsy so the element sizes to content again. Closes #318 Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
With fAutoExpandOnChildHit + fAutoSizeToFitChildren a drop emitted the transient soft-expand rect and then the settled auto-fit rect - two events per gesture. Skip the soft-expand emit when the target also auto-fits; groups that only auto-expand keep emitting it. Closes #319 Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
The controller subscribes to every node's and group's sizeChange and forwards it to applyResize, which merges the new geometry into the current shape WITHOUT its own history step (amendCurrent). So a group auto-fitting after a child was added rides along with the action that triggered it: one undo reverts both, and snapshot() carries the settled size. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
Selection is now an undoable step out of the box (Figma/Photoshop
style), and a drag's leading selection folds into the same step as the
move. Pass withFlowState({ selectionInHistory: false }) to keep
selection out of the history (xyflow/tldraw style).
Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
Bind [fGroupSize]/[fNodeSize] to the state so undo restores container geometry, and refresh the managed-state docs for the on-by-default selection history and size capture. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
Add an `fDropToGroup` signal input on fDraggable (default true) that enables/disables the drop-to-group gesture at the library level. - When on, the flow carries a reactive `f-drop-to-group` host class and the grouping() SCSS highlight is scoped under it, so disabling the feature also removes its styling. - When off, DropToGroupPreparation registers an inert (empty-target) handler so the external-item finalize still finds one (it throws otherwise) but nothing reparents, nests, highlights, or emits fDropToGroup. Also adds a live 'Drop to Group' toggle to the drag-to-group example and documents the input across the guide, example, styling docs, STYLING.md and llms-full.txt. Note: `fDropToGroup` is intentionally both an input (toggle) and the existing output (event) — [fDropToGroup] binds the input, (fDropToGroup) binds the event. Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
The state plugin no longer reparents on a drop-to-group gesture unless
you opt in with `withFlowState({ dropToGroup: true })` — the default is
now false. Out of the box the gesture still fires (library-level
`fDropToGroup` stays on) but withFlowState ignores group membership: a
drop is a no-op and an external item lands at the top level.
- Flip the mergeFlowStateConfig default and update the field JSDoc +
applyDropToGroup/createNodeRecord comments.
- The flow-state example opts in via withFlowState({ dropToGroup: true }).
- Controller-spec reparenting tests now pass { dropToGroup: true }; the
plain setup() test asserts the new off-by-default behavior.
- Update the state example doc.
Co-authored-by: Siarhei Khudzinski <158575591+siarheikhudzinski@users.noreply.github.com>
- Introduced `getEventTargetElement` utility to resolve event targets across shadow boundaries. - Implemented `getDeepElementsFromPoint` for hit-testing in nested shadow roots. - Updated `isDragBlocker` to accept SVG elements. - Enhanced `isOnFlowBackground` to utilize the new event target resolution method. - Added badge to the state example in the documentation. - Improved connection redraw logic to optimize performance in large flows. Signed-off-by: Siarhei Huzarevich <shuzarevich@gmail.com>
siarheihuzarevich
force-pushed
the
feature/shuzarevich/v19.1.0
branch
from
July 13, 2026 10:39
db5d950 to
a1c6c82
Compare
siarheihuzarevich
marked this pull request as ready for review
July 13, 2026 10:48
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
provideFFlow(withFlowState())19.1.0package versionsManaged Flow State
Foblex Flow remains stateless by default. Applications can opt into a generic state and history layer while keeping ownership of business meaning, validation, persistence, and collaboration.
injectFlowState<TNode, TConnection, TGroup>()exposes flat, application-shaped node, group, and connection records without a requireddatawrapper. Supported pointer and programmatic changes update those records immutably, while public interaction outputs continue to fire for existing integrations.The history layer follows user actions rather than individual runtime events. Selection, movement, descendant transforms, auto-fit updates, and optional reparenting produced by one drag settle into one undo step.
state.batch(...)provides the same behavior for programmatic changes, andstate.changes()increments only after the outer batch settles so persistence receives one stable notification.Configuration covers history limits, selection and viewport participation, viewport debounce, drop-to-group, node and connection factories, and a custom
stateClass. Initial centering can stay out of history through the newemitCanvasChangeargument on canvas positioning methods.Managed State v1 does not automatically capture rotation, connection waypoint editing, or user resize. Their existing outputs remain available for explicit application updates. Connection cascade before connectors render requires removing known attached connection ids in the same batch.
Runtime And Correctness
Connection redraw requests now carry the affected geometry instead of invalidating every connection. Group changes include connections owned by descendant nodes, fixing stale paths after group movement and state restoration without returning to a full redraw.
The minimap uses cached model-space rectangles, registry changes are coalesced, connector border radii are cached, connectable-side recalculation shares one scheduler, and drag targeting follows DOM ancestry instead of scanning all nodes.
State restoration now explicitly completes the full-render lifecycle. Group auto-fit waits for settled parent ids, emits the final size through public outputs, clears stale bound dimensions, and avoids transient soft-expanded sizes.
Shadow DOM
Normal DOM interactions remain target-first. When a document-level event is retargeted to an Angular Element host, the runtime falls back to the composed path to recover the internal flow element. Coordinate hit-testing follows
elementsFromPoint()through nested open shadow roots while preserving browser hit order. Closed shadow roots remain unsupported.This addresses discussion #315.
Compatibility
Validation
NX_DAEMON=false npm run test:ci- 288 tests passed in ChromeHeadlessNX_DAEMON=false npm run build:packagesNX_DAEMON=false npm run build:portal:devnpm run seo:checknpm run lint:portalRelease Material