Foblex Flow
Introduction
Overview
Nodes
Custom Nodes
Drag Handle
Node Selection
Resize Handle
Rotate Handle
Grouping
Drag to Group
Connectors
Node as Connector
Connector Inside Node
Connector Outlet
Limiting Connections
Connection Rules
Connectable Side
Connections - Editing
Drag to Connect
Click to Connect
New
Drag to Reassign
Create Node on Connection Drop
Remove Connection on Drop
Assign Node to Connection on Drop
Auto Snap
Connections - Appearance
Connection Types (Straight, Segment, Bezier, Adaptive Curve)
Custom Connection Type
Connection Behaviours
Connection Markers
Connection Content
Connection Gradients
Connections - Routing
Connection Waypoints
Connection Connectable Side
Viewport
Minimap
Zoom
Background
Auto Pan
New
Canvas Layer Ordering
New
Editor Helpers
Selection Area
Magnetic Lines
Magnetic Rects
Grid System
Add Node from Palette
Accessibility
New
Help in Positioning (Legacy)
Deprecated
Layout Engines
Dagre Layout
Dagre Auto Layout
ELK.js Layout
ELK.js Auto Layout
Reflow
Reflow on Resize
Editor State
Cut/Copy/Paste
State
New
Events
Drag Start/End Events
Custom Event Triggers
Control Schemes
New
Performance
Large Scene Performance
Connection Redraw Performance
Reference Apps
AI Low-Code Platform
Schema Designer
Call Center Flow
UML Diagram
Tournament Bracket
Foblex Flow

Managed Flow State

Description

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

[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

Gestures apply themselves

The controller forwards the supported finished gestures into the store. All events belonging to one drag session are batched into one undoable history step:

  • Create connection — dragging from a connector to a target adds a connection record with a generated id; dropping into empty space adds nothing.
  • Reassign connection — dragging an endpoint to another connector updates sourceId/targetId.
  • Move nodes — a drag (including a multi-selection drag) writes all new positions as a single step, so one undo() returns the whole group.
  • Delete selection — the removal request (e.g. the 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.
  • Drop into a group — grouping is opt-in: it's off by default, so this example enables it with 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.
  • External item drop — an item dragged from a palette becomes a node at the drop position, with the item's fData spread onto the node as its own fields; dropped over a group it's nested there (because this example opted into dropToGroup).
  • Pan / zoom the canvas — the viewport transform is captured into 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.
  • Programmatic view changes — pass 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 back to the start — when 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.
  • Debounce the recording — a zoom fires a burst of changes; withFlowState({ canvasTransformDebounce: 200 }) collapses the burst into a single undo step once it settles (a drag pan already folds into one step).

Scope of v1

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.

Undo/Redo built in

  • state.undo() / state.redo() walk the history; canUndo / canRedo are signals, ready for [disabled] bindings.
  • Records are updated immutably, so a history entry is just an object reference — history is cheap regardless of graph size.
  • 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.
  • One drag session is one step. When a single gesture ends with several events — a move plus a drop into a group, or (with selectionInHistory) a selection plus a move — they are folded into a single undoable action, so one undo reverts the whole thing.

Reacting to changes

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.

Data in, data out

  • 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.
  • Groups are their own collection with the same shape, rendered with 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.

Configuration

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.

Core API

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.

Selection, and whether it belongs in the history

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:

  • On by default — every selection change is its own step and undo/redo restore the previous highlight (Figma, Photoshop); a drag's leading selection folds into the same step as the move.
  • Set false to keep selection out of the history, so undo walks only graph edits (xyflow, tldraw).

Programmatic editing shares the same history

Everything the gestures do is also available as methods — and every call is one undoable step:

Connection cascade requires rendered connectors

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:

Shaping and vetoing gesture results

For light-touch control, pass factories to the config — return null to reject:

Every behavior is overridable

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:

When to use it

  • Use the state plugin when you want the library to do the data bookkeeping: forms-over-flows, quick editors, AI-generated integrations that expect a nodes[]/edges[]-style API.
  • Keep the classic event-driven approach when the graph lives inside a larger application store you already maintain — every event keeps working exactly as before; the plugin is optional and additive.

Related examples

Edit this page on GitHub