Originally published on JavaScript in Plain English:https://javascript.plainenglish.io/inside-foblex-flow-part-2-drag-and-drop-architecture-in-angular-without-cdk-bfe3dce097a3

When we think about node-based editors, the first thing that stands out is the interaction with on-screen elements. Dragging nodes, connecting ports, zooming the canvas — that’s what brings the interface to life.
Without a well-designed drag-and-drop system, the entire editor turns into a static image. That’s why, during the development of Foblex Flow, we decided to build our own engine for handling events and drag sessions.
🛠 Sources
At first glance, it might seem sufficient to use Angular CDK DragDrop. But in practice, its limitations became clear:
So we decided:
👉 We need a custom engine where we have full control over:
One of our core principles is that — whether it’s a mouse or touch input — the event should look the same higher up the stack.
To achieve this, we introduced the following interface:
Now, all logic in the editor operates with a unified event object.
Advantages of this approach:
Unified Event Interface
Drag-and-drop in Foblex Flow is structured in layers:
📌 This layered structure cleanly separates concerns:
Every interaction in Foblex Flow follows a consistent scenario:
The user presses the mouse button or touches the screen.
The directive checks the configured triggers to determine whether starting a move, resize, or connection is allowed at that moment.
A movement threshold (3 pixels by default) is intentionally applied to avoid triggering a drag from small cursor shakes.
Only when this threshold is exceeded does the drag session officially begin.
prepareDragSequence is invoked. This phase is triggered within mousemove/touchmove/pointermove, but only before the drag session actually starts.
Plugins like NodeMove, Resize, Rotate, CanvasMove, DropToGroup, and others evaluate whether they should participate. If so, they create their own motion handler.
Once at least one motion handler is registered, there’s no need to evaluate the rest — only one action is allowed per session (move, resize, or rotate).
The mousemove/touchmove/pointermove logic switches to passing delta movement data to the active handler instead of preparing new ones.
On mouseup/touchup/pointerup, finalization handlers are called. The result is committed — a node is moved, a connection is created, or an item is dropped into a group.
👉 Each drag session has clearly defined boundaries. This makes behavior predictable and debugging significantly easier.
Dragging isn’t just about “moving a node.” In real-world applications, the use cases are far more diverse:
Instead of hardcoding all of this into the core, we built a before/after plugin architecture:
Plugins can subscribe to lifecycle hooks like onPointerDown, prepareDragSequence, and onPointerUp.
This allows extending the functionality without touching the core logic.
📌 Example: you can add a hover highlight effect using a separate plugin — no need to modify the base code.
All drag events are handled inside ngZone.runOutsideAngular, which prevents unnecessary Angular change detection cycles.
This results in high performance, even with hundreds of active nodes.
2. OS-aware Triggers
On macOS, multi-selection uses metaKey; on Windows and Linux — ctrlKey.
This behavior is built into the fMultiSelectTrigger directive and delivers familiar UX across all platforms — right out of the box.
3. SSR Compatibility
We never access window or document directly.
All platform APIs are accessed through BrowserService, which can be replaced with a mock on the server.
This makes drag-and-drop work even in Angular Universal, without crashes or runtime errors.
4. Configurable Behavior
Any drag-and-drop behavior can be finely tuned using triggers. For example:
In this case, nodes will only move when the Shift key is held.
This gives developers full control over editor behavior.
5. Assistive Technology Protection
We’ve implemented checks to prevent drag sessions from being triggered by screen readers or other assistive technologies.
This is important for accessibility and ensures that interaction remains intentional.

Architectural Decisions
We’ve paid close attention to subtle details that directly affect the user experience:
After a touchstart, browsers often fire a fake mousedown.
To avoid double triggers, we introduced a MOUSE_EVENT_IGNORE_TIME = 800ms threshold during which such events are ignored.
During a drag session, the selectstart event is suppressed.
This prevents the user from accidentally selecting text while trying to drag an element — leading to a cleaner, more intuitive experience.
Drag-and-drop in Foblex Flow is not just about moving elements around — it’s a carefully designed architecture that offers stability, flexibility, and scalability:
This architecture forms a solid foundation for building not just a visual editor, but a complete platform — from simple node movements to complex scenarios like external drag sources, dynamic reconnection of links, and context-aware behaviors.