Foblex Flow
Articles
Overview
Libraries Compared (2026)
Releases
v19.1.0
v19.0.0
v18.6.0
v18.5.0
v18.4.0
v18.3.0
v18.2.0
v18.1.0
v18.0.0
v17.8.5
v17.8.0
v17.7.0
Feature Deep Dives
Keyboard Accessibility
Unified Connector Model
Control Schemes
Inside Foblex Flow
Part 1: Library Architecture and Design Principles
Part 2: Drag-and-Drop Architecture in Angular Without CDK
Part 3: Designing a Stateless Library
Building AI Low-Code Platform
Part 1: Introduction to Foblex Flow
Part 2: Creating Your First Flow
Part 3: Creating Custom Nodes and a Node Palette
Part 4: Styling and Handling Connections
Call Flow Editor
Angular 20 Update
Initial Tutorial
Foblex Flow

Designing a Stateless Library: How Foblex Flow Avoids Owning Your Data

The problem with libraries that own your data

Most node-based libraries ship with an opinion about your data model. You hand them a nodes[] array and an edges[] array, and in return you get a rendered graph. That works fine in a demo.

It starts to break the moment your app grows up.

Your real domain model is not { id, x, y }. It is a workflow step, a call routing rule, an AI agent with a schema, a piece of business logic with validations attached.

Persistence is not "serialize the library's internal state to JSON". It is writing to your backend, your database, your existing document format.

In the default stateless setup, undo/redo is not the library's job. It is a feature of your editor, tied to your command history.

Optimistic updates, collaboration, offline sync — all of that lives in your app, not in a graph library.

When a library owns the data, every one of those concerns has to go through an adapter. You spend more time translating between the library's model and yours than you spend building the actual editor.

What Foblex Flow does instead

Foblex Flow's core is stateless by default.

The library does not hold the definition of your graph. It renders what you pass in as Angular template children, listens for user interaction on those children, and emits events when something happens. Your application decides whether to apply the change.

That is the whole model.

  • The library renders the UI layer.
  • The application owns the business logic.
  • The two communicate through events and inputs, without a hidden internal store of your data.

This is not just a design preference. It is the decision that shaped every API that came after.

Optional managed state, without changing the core

The default contract above remains unchanged. If an application wants the library to handle common graph bookkeeping, it can explicitly install the optional Managed Flow State plugin:

withFlowState() provides typed node, group, connection, selection and viewport signals, plus load(), snapshot() and batched undo/redo. Supported finished gestures update that opt-in store automatically. The same public events still fire, records keep the application's own fields, and the application can override or replace the default mutation behavior.

So "stateless" describes the core and the default integration mode. Managed state is an explicit provider installed by applications that prefer convenience over wiring every supported gesture themselves; it is not a hidden store imposed on every flow.

šŸŽÆ The key idea — what the library stores vs what the app stores

Most graph libraries keep a copy of your nodes and edges inside themselves. The library becomes the source of truth. Your app mirrors it.

Foblex Flow takes a different approach.

What the core stores in the default mode:

  • References to the <f-node> and <f-connection> instances currently in the template
  • Transform state of the canvas — zoom, pan, viewport size
  • Interaction state during a gesture — what is being dragged, resized, rotated, or reassigned
  • Selection state — which node and connection IDs are currently highlighted
  • Internal caches needed to draw connections and run hit-testing

What the app stores in the stateless mode:

  • The actual list of nodes and their properties
  • The actual list of connections
  • Anything domain-specific — labels, step types, validation rules, metadata
  • Positions, if you want them persisted (the library tracks them during a drag, but the canonical value lives in your state)
  • History, for undo/redo (unless the optional managed-state plugin owns this generic graph history)

šŸ“Œ In short: the library handles the UI layer, while your application owns the business logic.

The managed-state plugin can own graph records and interaction history, but it never owns what those records mean, how they are validated, or where they are persisted.

⚔ The event-driven model

The golden rule of the stateless core: the library never mutates your application-owned data silently.

Every user interaction that would change the graph is surfaced as an event. In this mode, the library does not write back into your signals or services. It waits.

Concrete events on the fDraggable directive:

  • fCreateConnection — the user finished dragging a line from one connector to another.
  • fReassignConnection — the user detached an existing connection and attached it somewhere else.
  • fCreateNode — an external item was dropped onto the canvas.
  • fMoveNodes — a drag that moved one or more nodes finished, with the final positions.
  • fConnectionWaypointsChanged — the user edited the waypoints of a connection.
  • fSelectionChange — the set of selected nodes and connections changed.
  • fDropToGroup — a node was dropped into a group.

Node position, size, and rotation follow the same pattern. fNodePosition is a two-way binding, but the source of truth is still your signal. The library reports the new value at the end of a gesture. Writing it back is your call.

Rendering lifecycle is also surfaced as events on the component:

  • fNodesRendered — all nodes have been measured and laid out.
  • fFullRendered — nodes and connections have both finished rendering. This is the signal to run "fit to content" after loading a flow.

This happened. You decide what to do.

That one sentence is the whole interaction contract.

Installing withFlowState() is an explicit variation of that contract: supported gestures are applied to the provided store automatically, while their public events remain observable.

🧩 How this plays out in practice

This is where the shape of the API diverges from what people expect the first time they try the library.

People usually reach for something like setState(…) or loadFromJSON(…). A method on the component that takes the full graph and renders it. That is the reducer-for-graphs mental model.

The core component does not work that way.

āŒ How people expect the API to work:

āœ… How the library actually works:

In the default mode there is no setState and no hidden graph store. The template is the graph — driven by your signals, rendered by Angular, with the library wiring up interactivity on top.

Initialization works the same way in stateless mode. You hydrate your own signals from your backend or file, Angular renders the template, the library picks up the <f-node> and <f-connection> children and makes them interactive. When rendering settles, fFullRendered fires — that is the place to call getState() for measured bounds and run "fit to content".

With Managed Flow State, initialization becomes state.load(...), the template renders state.nodes(), state.groups() and state.connections(), and state.snapshot() returns persistable graph records. The application still decides when to load and save them.

It is more accurate to say the Foblex Flow core is an interaction layer than a graph library. The graph is yours unless you explicitly place its generic records in Managed Flow State.

šŸ›  Why this decision was made

Every consequence of "stateless core" looks, on the surface, like a missing feature. None of them are.

Persistence is your job. The stateless core does not ship a toJSON() that serializes your graph, because it does not have your graph. Managed Flow State offers snapshot() for its opt-in records, but your application still decides the domain shape, storage format and persistence lifecycle.

Undo/redo belongs to the state owner. In stateless mode, that is your application's command history: it knows that adding a node, editing a label or changing a validation rule are meaningful actions. Managed Flow State provides a generic history stack for supported graph gestures and mutations when that is enough. Domain actions outside the plugin remain the application's responsibility.

Optimistic updates are your job. In stateless mode, write to your signal, let the view update, and reconcile later if the server disagrees. With Managed Flow State, reconcile against the explicit plugin store instead; it is still application-scoped and inspectable rather than hidden runtime data.

Collaboration is your job. Two users dragging the same node is a merge problem in your domain, not a rendering problem. The library hands you the gesture; your CRDT or your OT layer decides what it means, then writes to your state, and the view follows.

That sounds restrictive in a feature matrix, but it matters in real editors. The apps I have seen built on Foblex Flow all have non-trivial domain models — call routing, AI agent graphs, workflow automation, ETL pipelines. In every one of them, the data model existed before the editor did. A library that insisted on owning it would have been a wall, not a tool.

🧠 Philosophy

A node editor library can do one of two things. It can be a platform that owns the graph and hands you hooks into it. Or it can be an interaction layer that renders what you already have.

Foblex Flow's core chose the second one on purpose. The library is not a small application you embed. It is a set of Angular primitives — <f-flow>, <f-canvas>, fNode, <f-connection> — that turn your existing data into something a user can drag, connect, and edit. Managed Flow State is an optional companion for applications that want those primitives plus a ready-made graph store.

In stateless mode, every event the library emits is an offer. This happened. You decide what to do with the change. Accept it by writing to your state and the view updates. Ignore it and nothing changes. Veto it by not writing, and the user's gesture was advisory. With Managed Flow State, supported gestures use the plugin's overridable handlers instead of waiting for application writeback.

For me, this is the point. An editor is a conversation between a user's intent and an application's rules. The stateless core reports gestures and waits. The managed-state plugin applies only the supported behavior an application explicitly opted into and exposes override points where its rules differ.

Small, but it shapes everything downstream. The stateless default is why persistence, collaboration and domain-specific history can stay in your app instead of fighting a hidden store. Managed Flow State is there when you deliberately want the library to own common graph records and interaction history without changing that default for everyone else.

šŸš€ What's next

This is Part 3 of the Inside Foblex Flow series.

Part 4 will look at the rendering pipeline: how <f-node> and <f-connection> elements are picked up from the template, how measurement and layout are coordinated, and how fNodesRendered and fFullRendered actually get decided under the hood.

If you're building a visual editor in Angular and want a native Angular solution (not a React wrapper) — take a look.

And if you like what I'm building, please consider starring the repo ⭐

It helps the project a lot.

Links

In this article