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.
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.
Foblex Flow is stateless.
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.
This is not just a design preference. It is the decision that shaped every API that came after.
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 library stores:
<f-node> and <f-connection> instances currently in the templateWhat the app stores:
📌 In short: the library handles the UI layer, while your application owns the business logic.
The golden rule of Foblex Flow: the library never mutates your data silently.
Every user interaction that would change the graph is surfaced as an event. The library does not write back into your signals or services. It waits.
Concrete events on the fDraggable directive:
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
This happened. You decide what to do.
That one sentence is the whole interaction contract.
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.
Foblex Flow does not work that way.
❌ How people expect the API to work:
✅ How the library actually works:
There is no setState. There is 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. You do not load a flow into the library. 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".
It is more accurate to say Foblex Flow is an interaction layer than a graph library. The graph is yours.
Every consequence of "stateless core" looks, on the surface, like a missing feature. None of them are.
Persistence is your job. The library does not ship a toJSON() that serializes your graph, because it does not have your graph. You already know how to persist your own data model. Any format the library invented would be a second one to maintain, and it would never fit your domain as well as the shape you already use.
Undo/redo is your job. The library does not ship a history stack. Your app's command history is already the thing that knows what a meaningful "action" is — adding a node, editing a label, changing a validation rule. A library-level undo would either duplicate that or fight with it. The library supports undo/redo by keeping the rendered state a pure function of your data: roll back the data, the view rolls back.
Optimistic updates are your job. Because the library never mutates silently, optimistic UI is straightforward — write to your signal, the view updates, reconcile later if the server disagrees. There is no internal library state to keep in sync with the server truth.
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.
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 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.
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.
For me, this is the point. An editor is a conversation between a user's intent and an application's rules. A library that silently applies every gesture cuts the application out of that conversation. A library that reports gestures and waits keeps the application in charge.
Small, but it shapes everything downstream. The stateless choice is why persistence, undo/redo, collaboration, and optimistic updates all live where they already lived — in your app — instead of fighting a hidden store inside the library.
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.
<f-flow> component: flow.foblex.com/docs/f-flow-componentfNode directive: flow.foblex.com/docs/f-node-directive<f-connection> component: flow.foblex.com/docs/f-connection-component