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 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;}
Context and Helpers
Section titled “Context and Helpers”The context provides access to the current state, configuration, and metadata about what triggered the change. Key properties include:
state
- Current diagram state with nodes, edges, and metadata.modelActionType
- What action triggered this middleware (e.g., ‘addNode’, ‘moveNodes’).nodesMap
/edgesMap
- Maps of nodes and edges by ID for quick lookup. Lookup is updated after each middleware.config
- Current diagram configuration.helpers
- Utility functions for checking what changed.
There are also more specialized properties depending on the action type, such as access to ActionStateManager, which provides temporary state for actions like resizing or linking.
The helpers object provides convenient methods to inspect changes:
helpers.anyNodesAdded()
- Check if any nodes were addedhelpers.checkIfNodeChanged(id)
- Check if specific node was modifiedhelpers.checkIfAnyNodePropsChanged(['position', 'size'])
- Check if any node had specific properties changed
Use next()
to continue to the next middleware, optionally passing state updates. Use cancel()
to abort the operation entirely.
Managing Middlewares
Section titled “Managing Middlewares”Registering Middlewares at the Start
Section titled “Registering Middlewares at the Start”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]); // ...}
Registering Middlewares at Runtime
Section titled “Registering Middlewares at Runtime”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: Custom Middleware
Section titled “Example: Custom Middleware”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 state by passing updates to next()
:
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