Middlewares
Middlewares provide a powerful plugin architecture for extending ngDiagram behavior. They intercept state changes before they reach the model, allowing you to transform data, add custom logic, and implement features without modifying core code.
How Middlewares Work
Section titled “How Middlewares Work”When any change occurs in the diagram (adding nodes, moving elements, changing selections), the change goes through a middleware pipeline before reaching the model. Each middleware can:
- Inspect the current state and what’s being changed
- Transform the data being applied
- Add additional changes to the state
- Cancel the operation entirely
- Perform asynchronous operations
Middlewares execute in sequence, with each middleware receiving the output of the previous one. This creates a powerful composition system where multiple behaviors can be combined.
Middleware API
Section titled “Middleware API”Middleware Action Types
Section titled “Middleware Action Types”The ModelActionType type lists all possible actions that can trigger middleware execution.
These represent every operation that modifies the diagram state, such as adding nodes, moving elements, deleting selections, resizing, linking, rotating, and more.
Middleware Interface
Section titled “Middleware Interface”Each middleware implements the Middleware interface:
interface Middleware<TName extends string = string> { name: TName; execute: ( context: MiddlewareContext, next: (stateUpdate?: FlowStateUpdate) => Promise<FlowState>, cancel: () => void ) => Promise<void> | void;}Middleware Context
Section titled “Middleware Context”The context object passed to middleware functions provides comprehensive access to the diagram state and metadata:
initialState– The state before any modificationsstate– The current state after all previous modifications.nodesMap/edgesMap– Maps for quick lookup of nodes and edges by IDinitialNodesMap/initialEdgesMap– Maps for accessing nodes/edges before any changesmodelActionType– The action that triggered the middleware.helpers– Utility functions for inspecting what changedhistory– Array of all state updates made by previous middlewares in the chain.actionStateManager– Manager for temporary action states.edgeRoutingManager– Manager for edge routing algorithms.initialUpdate– The initial state update that triggered the middleware chain.config– Current diagram configuration.environment– Environment information (browser, rendering engine, etc.).
Middleware Helpers
Section titled “Middleware Helpers”The helpers object provides optimized functions to inspect all cumulative changes from the initial update and previous middlewares:
checkIfNodeChanged(id)– Was this node modified?checkIfEdgeChanged(id)– Was this edge modified?checkIfNodeAdded(id)/checkIfNodeRemoved(id)– Was this node added/removed?checkIfEdgeAdded(id)/checkIfEdgeRemoved(id)– Was this edge added/removed?checkIfAnyNodePropsChanged(['prop1', 'prop2'])– Did any node have these properties changed?checkIfAnyEdgePropsChanged(['prop1', 'prop2'])– Did any edge have these properties changed?anyNodesAdded()/anyEdgesAdded()– Were any nodes/edges added?anyNodesRemoved()/anyEdgesRemoved()– Were any nodes/edges removed?getAffectedNodeIds(['prop1', 'prop2'])– Get IDs of nodes with specified properties changed.getAffectedEdgeIds(['prop1', 'prop2'])– Get IDs of edges with specified properties changed.getAddedNodes()/getAddedEdges()– Get instances of added nodes/edges.getRemovedNodes()/getRemovedEdges()– Get instances of removed nodes/edges (from initial state).
These helpers allow you to efficiently check what changed during the middleware execution chain.
Example: Using Helpers
Section titled “Example: Using Helpers”// Example with a middleware that checks if any node was added into a groupexport const myMiddleware: Middleware = { name: 'group-node-checker', execute: async (context, next) => { const { helpers } = context; if (helpers.checkIfAnyNodePropsChanged(['groupId']).length > 0) { const affectedNodeIds = helpers.getAffectedNodeIds(['groupId']); console.log('Nodes were added to groups:', affectedNodeIds); }
next(); // Continue to next middleware },};Middleware History
Section titled “Middleware History”The history array in the context tracks all state updates made by previous middlewares, including the name of the middleware and the specific state update applied.
This is useful for auditing or debugging complex middleware chains.
Managing Middlewares
Section titled “Managing Middlewares”Initial Registration
Section titled “Initial Registration”The primary way to configure middlewares is using the createMiddlewares helper and passing them to the <ng-diagram> component:
import { createMiddlewares } from 'ng-diagram';
// Use all default middlewaresconst middlewares = createMiddlewares((defaults) => defaults);
// Add custom middleware to the chainconst middlewares = createMiddlewares((defaults) => [...defaults, myCustomMiddleware]);
// Remove specific middlewareconst middlewares = createMiddlewares((defaults) => defaults.filter((m) => m.name !== 'logger'));Then pass them to your diagram component:
@Component({ template: ` <ng-diagram [model]="model" [config]="config" [middlewares]="middlewares" /> `,})export class MyDiagramComponent { middlewares = createMiddlewares((defaults) => [...defaults, myMiddleware]); // ...}Runtime Registration
Section titled “Runtime Registration”You can also register and unregister middlewares dynamically using the NgDiagramService.
Note: This can only be done when the diagram is initialized, which you can check using isInitialized signal:
import { inject } from '@angular/core';import { NgDiagramService } from 'ng-diagram';
@Component({...})export class MyComponent { private ngDiagram = inject(NgDiagramService);
addMiddleware() { // Check if diagram is initialized first if (!this.ngDiagram.isInitialized()) { console.warn('Cannot register middleware: diagram not initialized'); return; }
// Register returns an unregister function const unregister = this.ngDiagram.registerMiddleware(myMiddleware);
// Later you can unregister the middleware by invoking the returned function // unregister(); }
removeMiddleware() { if (!this.ngDiagram.isInitialized()) { console.warn('Cannot unregister middleware: diagram not initialized'); return; }
// You can also unregister the middleware by name this.ngDiagram.unregisterMiddleware('my-middleware-name'); }}Creating Custom Middlewares
Section titled “Creating Custom Middlewares”Basic Middleware Structure
Section titled “Basic Middleware Structure”Here’s the basic structure for a custom middleware:
import { Middleware } from 'ng-diagram';
export const myMiddleware: Middleware = { name: 'my-middleware', execute: async (context, next) => { const { state, modelActionType } = context;
// Check if this middleware should run if (modelActionType !== 'updateNode') { next(); // Pass through without changes return; }
// Your custom logic here console.log('Nodes being updated:', state.nodes);
// Continue to next middleware next(); },};Example
Section titled “Example”For a complete example of creating custom middleware, see the Custom Middleware example which demonstrates how to implement a read-only mode middleware that prevents certain operations while allowing others.
Advanced Middleware Features
Section titled “Advanced Middleware Features”Modifying State
Section titled “Modifying State”Middlewares can modify the diagram state by passing a FlowStateUpdate object to next():
interface FlowStateUpdate { nodesToAdd?: Node[]; nodesToUpdate?: (Partial<Node> & { id: Node['id'] })[]; nodesToRemove?: string[]; edgesToAdd?: Edge[]; edgesToUpdate?: (Partial<Edge> & { id: Edge['id'] })[]; edgesToRemove?: string[]; metadataUpdate?: Partial<Metadata>;}This allows you to add, update, or remove nodes/edges, and update metadata in a granular way.
Example: Rotating New Nodes
Section titled “Example: Rotating New Nodes”export const myCustomMiddleware: Middleware = { name: 'rotate-middleware', execute: async (context, next) => { const { helpers } = context;
// Check if this middleware should run if (!helpers.anyNodesAdded()) { next(); // Pass through without changes return; }
// Rotate all new nodes and change label const stateUpdate = { nodesToUpdate: context.state.nodes .filter((node) => helpers.checkIfNodeAdded(node.id)) .map((node) => ({ ...node, angle: 45, data: { ...node.data, label: `rotated via middleware` }, })), };
// Continue to next middleware next(stateUpdate); },};Asynchronous Operations
Section titled “Asynchronous Operations”Middlewares can perform async operations:
export const myCustomMiddleware: Middleware = { name: 'validation', execute: async (context, next, cancel) => { const { helpers } = context;
if (!helpers.anyNodesAdded()) { next(); return; }
// Simulated API validation function const validateNodesWithAPI = () => { return new Promise<void>((resolve, reject) => { setTimeout(() => { const isValid = Math.random() > 0.5; // 50% chance of success if (isValid) { resolve(); } else { reject(new Error('Random validation failure')); } }, 1000); // 1 second delay — after this time the node will be added if valid, or an alert/error will be displayed if invalid }); };
try { await validateNodesWithAPI(); next(); // Validation passed } catch (error) { alert(`Validation failed: ${error}`); cancel(); // Cancel the operation } },};However, you should avoid long-running async operations in middlewares as they can block the UI.
Best Practices
Section titled “Best Practices”Performance
Section titled “Performance”- Check conditions early: Return
next()immediately if your middleware doesn’t need to run - Use helpers efficiently: The helper functions are optimized for checking what changed
- Avoid heavy computations: Keep middleware logic lightweight, especially for frequently-triggered actions
Middleware Ordering
Section titled “Middleware Ordering”- Validation middlewares should run early to fail fast
- Data transformation middlewares should run before built-in middlewares that depend on the data
- Logging middlewares typically run last to capture the final state