provideFFlow(withFlowState()) turns on the graph state built into Foblex Flow. The application loads plain data once, renders the nodes, groups and connections signals with @for, and reads the data back whenever it needs to persist — there is not a single gesture handler in the component below.
Your records are your own shape: extend the framework interface with any fields — no data wrapper.
Drag an item from the palette to add a node, drop a node into the group to nest it, drag between connectors to wire them, or select and press Delete: these supported gestures land in the state as undoable steps without component event handlers.
[example.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/libs/f-examples/advanced/flow-state/example.html [example.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/libs/f-examples/advanced/flow-state/example.ts [example.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/libs/f-examples/advanced/flow-state/example.scss
The controller forwards the supported finished gestures into the store. All events belonging to one drag session are batched into one undoable history step:
sourceId/targetId.undo() returns the whole group.Delete key from the accessibility layer) removes selected records. Because this gesture runs against a rendered flow, the connector registry is available and attached connections cascade with their nodes/groups.withFlowState({ dropToGroup: true }). With it on, dropped nodes and groups get the group's id as parentId; leave it off and the drop is a no-op and external items land at the top level.fData spread onto the node as its own fields; dropped over a group it's nested there (because this example opted into dropToGroup).state.transform() and is undoable by default. Bind the canvas [position]/[scale] to state.transform() (as this example does) so undo/redo move the view back. Set withFlowState({ canvasTransformInHistory: false }) to keep pan/zoom out of history — it's still tracked and saved in snapshot() either way. position starts undefined (before any transform), which leaves [position] free for the initial resetScaleAndCenter.false as the emitCanvasChange argument to resetScaleAndCenter, fitToScreen or centerGroupOrNode when a library-driven move must stay out of state history. This example initializes the viewport with resetScaleAndCenter(false, false).undo returns to the beginning of history, the flow's reset() re-runs the full-render lifecycle, so the app's fFullRendered handler (resetScaleAndCenter here) runs again.withFlowState({ canvasTransformDebounce: 200 }) collapses the burst into a single undo step once it settles (a drag pan already folds into one step).Managed state v1 does not capture rotation, connection waypoint editing, or user resize. Those interactions continue to update the rendered directives and emit their normal public outputs; applications that enable them must update their own records. Library-driven size measurement and group auto-fit may amend the current stored geometry, but they do not create a separate user history step.
state.undo() / state.redo() walk the history; canUndo / canRedo are signals, ready for [disabled] bindings.withFlowState({ historyLimit: 100 }) caps the stack (default 50); the oldest steps fall off.load() replaces the whole graph and resets the history — ideal for opening a document; clearHistory() keeps the data and drops only the steps.selectionInHistory) a selection plus a move — they are folded into a single undoable action, so one undo reverts the whole thing.state.changes() ticks when a standalone mutation or an outer batch settles, and on undo, redo or load. A multi-event gesture such as selection plus move increments it once when the gesture finishes. Depend on it from a single effect to react to anything that touches the graph, without wiring one listener per gesture:
The store changes its collection signals immediately while a gesture is in progress, but delays the changes() increment until the outer batch closes. Standalone mutations notify immediately. The per-collection signals (nodes(), groups(), connections(), selection()) remain available when you only care about one slice.
load({ nodes, groups, connections }) — plain arrays in. The store only reads the framework keys (id, position, optional size, rotate, parentId) and carries the rest of your record through untouched — put your fields right on it, no data wrapper.fGroup — a first-class kind because the library treats them separately (a group id in a selection, its own drop target). Nesting is just parentId.snapshot() — plain arrays out (nodes, groups, connections) with copied geometry; persist the result as-is.injectFlowState<MyNode, MyConnection, MyGroup>() types all three end to end; each argument defaults to the framework shape, so injectFlowState<MyNode>() is enough when only nodes carry extra fields.| Option | Default | Meaning |
|---|---|---|
historyLimit |
50 |
Maximum number of undo steps. |
selectionInHistory |
true |
Include selection in undo/redo; a drag's leading selection is batched with the move. |
canvasTransformInHistory |
true |
Include user pan/zoom in undo/redo. The transform is still tracked when disabled. |
canvasTransformDebounce |
0 |
Debounce canvas events before committing a viewport history step. |
dropToGroup |
false |
Reparent dropped records and nest external items into groups. |
connectionFactory |
generated id/endpoints | Create or veto a gesture-created connection. |
nodeFactory |
payload plus normalized drop rect | Create or veto a node dropped from an external palette. |
stateClass |
FFlowState |
Install an application subclass that overrides store behavior. |
| API | Purpose |
|---|---|
nodes, groups, connections |
Readonly collection signals rendered by the template. |
selection, transform |
Current selection and canvas transform signals. |
canUndo, canRedo, changes |
History availability and settled-change notification signals. |
load(data), snapshot() |
Replace/reset the store and export persistable data. |
getNode, getGroup, getConnection |
Read one record by id. |
add*, update*, remove*, moveNodes |
Programmatic immutable mutations. |
undo, redo, clearHistory |
History operations. |
batch(work) |
Collapse several mutations into one undo step and one changes() increment. |
state.selection() always reflects the current selection (nodeIds, groupIds, connectionIds) as a signal — handy for a properties panel or a context menu. Whether selecting is undoable is a config switch, because editors disagree:
undo/redo restore the previous highlight (Figma, Photoshop); a drag's leading selection folds into the same step as the move.false to keep selection out of the history, so undo walks only graph edits (xyflow, tldraw).Everything the gestures do is also available as methods — and every call is one undoable step:
Connections store connector ids (sourceId/targetId), not owner node ids. While the flow is rendered, the controller resolves each connector to its node/group and removeNodes/removeGroups automatically remove attached connections. Before connector directives have registered (for example, programmatic editing immediately after load() or during SSR), that ownership map does not exist.
When removing records before render, compute the attached connection ids from your data and delete both inside one batch:
For light-touch control, pass factories to the config — return null to reject:
If a default does not fit, subclass the store — any CRUD method, any apply* gesture handler, any protected building block — and install your class. The auto-wiring dispatches through it:
nodes[]/edges[]-style API.