Skip to content

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.

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.

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.

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;
}

The context object passed to middleware functions provides comprehensive access to the diagram state and metadata:

  • initialState – The state before any modifications
  • state – The current state after all previous modifications.
  • nodesMap / edgesMap – Maps for quick lookup of nodes and edges by ID
  • initialNodesMap / initialEdgesMap – Maps for accessing nodes/edges before any changes
  • modelActionType – The action that triggered the middleware.
  • helpers – Utility functions for inspecting what changed
  • history – 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.).

The helpers object provides optimized functions to inspect all cumulative changes from the initial update and previous middlewares:

These helpers allow you to efficiently check what changed during the middleware execution chain.

// Example with a middleware that checks if any node was added into a group
export 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
},
};

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.

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 middlewares
const middlewares = createMiddlewares((defaults) => defaults);
// Add custom middleware to the chain
const middlewares = createMiddlewares((defaults) => [...defaults, myCustomMiddleware]);
// Remove specific middleware
const 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]);
// ...
}

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');
}
}

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();
},
};

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.

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.

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);
},
};

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.

  • 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
  • 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