v19.1 makes the newer interaction architecture practical in production editors. The main addition is an opt-in state layer that coordinates graph mutations and history without forcing a domain model on the application. The release also reduces repeated runtime work in large diagrams and adds interaction support for Angular Elements and open Shadow DOM.
The Foblex Flow core remains stateless by default. Existing event-driven integrations keep working unchanged, and there are no breaking changes in this release.
Managed Flow State
Opt in instead of adopting a mandatory store
Install the state plugin only in editors that need it:
import { IFStateNode, injectFlowState, provideFFlow, withFlowState } from '@foblex/flow';
interface WorkflowNode extends IFStateNode {
label: string;
kind: 'trigger' | 'action';
}
@Component({
providers: [provideFFlow(withFlowState())],
})
export class WorkflowEditor {
readonly state = injectFlowState<WorkflowNode>();
constructor() {
this.state.load({
nodes: [],
groups: [],
connections: [],
});
}
}State records stay flat and application-shaped. Extend IFStateNode, IFStateGroup, and IFStateConnection with domain fields directly; no required data wrapper is introduced. injectFlowState<TNode, TConnection, TGroup>() preserves those record types through reads, mutations, history, and snapshots.
The plugin owns generic editor bookkeeping. The application still owns what records mean, validation rules, server synchronization, collaboration, and persistence. Public interaction outputs continue to fire, so the managed path does not close off existing event-driven integrations.
Supported interactions update the records automatically
Managed state v1 coordinates the repetitive changes normally implemented around flow outputs:
- connection creation and endpoint reassignment;
- node and group movement, including multi-selection drags;
- selection changes;
- deletion of rendered nodes, groups, and connections;
- external palette drops through a configurable
nodeFactory; - optional drop-to-group reparenting;
- canvas pan and zoom;
- immutable programmatic
add*,update*,remove*, andmoveNodescalls.
The state signals can be rendered directly:
@for (node of state.nodes(); track node.id) {
<div fNode [fNodeId]="node.id" [fNodePosition]="node.position">
{{ node.label }}
</div>
}
@for (connection of state.connections(); track connection.id) {
<f-connection
[fConnectionId]="connection.id"
[fSourceId]="connection.sourceId"
[fTargetId]="connection.targetId"
/>
}History follows user actions, not individual events
A single drag can select a node, emit several position updates, move group descendants, update an auto-fit group, and finish by reparenting the node. Those are separate runtime changes but one user action. Managed Flow State keeps the batch open for the whole gesture, so one undo() reverts the complete result and redo() reapplies it as one step.
Application code can use the same model with state.batch(...). Several programmatic mutations inside the batch produce one history entry and one settled change notification. historyLimit bounds retained entries, while selectionInHistory and canvasTransformInHistory independently control whether selection and viewport changes participate.
Stable persistence notifications
state.changes() increments when the outer action batch settles, not for every internal update. Persistence code therefore receives one stable notification for the completed action:
effect(() => {
this.state.changes();
this.persist(untracked(() => this.state.snapshot()));
});Use snapshot() for persistable graph, selection, and viewport data, and load() to replace the managed state. A custom stateClass can override store behavior when an application needs different mutation or history rules without forking the interaction pipeline.
Viewport history without initialization noise
User pan and zoom are recorded by default. Initial centering usually is not a user action, so resetScaleAndCenter, fitToScreen, and centerGroupOrNode now accept an optional emitCanvasChange argument:
flow.resetScaleAndCenter(false, false);Passing false keeps initialization and other application-driven viewport changes out of external and managed history. canvasTransformDebounce controls how nearby viewport updates are grouped.
Explicit configuration
withFlowState(...) supports configuration for:
historyLimit;selectionInHistory;canvasTransformInHistoryandcanvasTransformDebounce;dropToGroup;nodeFactoryandconnectionFactory;- a custom
stateClass.
Managed reparenting is intentionally separate from the gesture switch. fDropToGroup controls whether the interaction is available and remains enabled by default for compatibility. withFlowState({ dropToGroup: true }) separately allows the state plugin to apply the emitted reparenting.
Managed State v1 Boundaries
Rotation, connection waypoint editing, and user resize are not captured automatically in v1. Their existing public outputs remain available, so applications can update managed records explicitly when those interactions are enabled.
Automatic connection cascade on node or group removal resolves ownership from the rendered connector registry. Before connectors render, including immediate post-load() changes and SSR, remove known attached connection ids explicitly inside the same state.batch(...) call.
Faster Large Flows
- Connection redraws are scoped to affected geometry instead of scheduling every connection in the flow.
- Moving a group also redraws connections owned by descendant nodes, keeping group movement and undo/redo restores correct.
- The minimap draws from cached model-space rectangles instead of measuring every node element during each redraw.
- Registry removals and component-store notifications are batched.
- Pointer targeting uses DOM ancestry instead of scanning every registered node.
- Connector border radii are cached, and connectable-side recalculation uses one shared scheduler across drag handlers.
These are internal runtime improvements and require no new configuration.
Angular Elements and Open Shadow DOM
Pointer gestures, connection targeting, external drops, background detection, and coordinate hit-testing now work when a flow is rendered in an open shadow root through Angular Elements or ViewEncapsulation.ShadowDom.
Normal DOM keeps target-first behavior. The runtime falls back to composedPath() only when document-level Shadow DOM retargeting hides the element inside the flow. Coordinate hit-testing follows elementsFromPoint() into nested open shadow roots while preserving native hit order. Closed shadow roots remain unsupported because their internal tree is intentionally inaccessible.
This work addresses discussion #315.
Correctness Fixes
- Connections attached to descendant nodes redraw after a group move or managed state restore.
- Undoing to the initial loaded state reruns the full-render lifecycle.
- Group auto-fit waits for settled parent ids, emits the final
fNodeSizeChange/fGroupSizeChange, and removes stale explicit dimensions when a bound size is cleared. - Combining
fAutoExpandOnChildHitwith auto-fit no longer emits a transient size before the settled group size.
Upgrade Notes
- There are no breaking changes in v19.1.0.
withFlowState()is opt-in; existing stateless and event-driven integrations keep working.- Open Shadow DOM support is automatic. Closed shadow roots remain unsupported.
- Pass
emitCanvasChange: falsefor initialization-time centering that should not enter viewport history. - Enable managed reparenting explicitly with
withFlowState({ dropToGroup: true }).
Full details: CHANGELOG | Release post
