Custom Middleware
This example demonstrates how to create a custom middleware that implements read-only functionality for ngDiagram. Middlewares provide a powerful plugin architecture for extending diagram behavior by intercepting state changes before they reach the model.
The read-only middleware demonstrates how to:
- Block specific actions based on configuration
- Allow selective operations through an allowlist
- Cancel operations to prevent state changes
- Log warnings for blocked actions
The middleware prevents users from modifying the diagram while still allowing actions like selection changes.
import type { Middleware } from 'ng-diagram';
// Define actions that should be blocked in read-only modeconst blockedActions = new Set([ 'changeSelection', 'moveNodesBy', 'deleteSelection', 'addNodes', 'updateNode', 'updateNodes', 'deleteNodes', 'clearModel', 'addEdges', 'updateEdge', 'deleteEdges', 'deleteElements', 'paste', 'resizeNode', 'startLinking', 'moveTemporaryEdge', 'finishLinking', 'changeZOrder', 'rotateNodeTo', 'highlightGroup', 'highlightGroupClear', 'treeLayout', 'moveNodes', 'moveNodesStop',]);
/** * Read-only middleware implementation that blocks specific actions when enabled */export const readOnlyMiddleware: Middleware<'read-only'> = { name: 'read-only', execute: (context, next, cancel) => { const { modelActionType, config } = context;
// Get read-only configuration const readOnlyConfig = (config as any).readOnly;
if (!readOnlyConfig?.enabled) { next(); // Not in read-only mode, allow everything return; }
// Allow specific actions if configured const allowedActions = readOnlyConfig.allowedActions || []; const isActionBlocked = blockedActions.has(modelActionType) && !allowedActions.includes(modelActionType);
if (isActionBlocked) { console.warn(`🔒 Action "${modelActionType}" blocked by read-only mode`); cancel(); // Cancel the operation return; }
// Allow the action to proceed next(); },};
import { ChangeDetectionStrategy, Component, inject, signal,} from '@angular/core';import { createMiddlewares, initializeModel, NgDiagramComponent, NgDiagramService, type NgDiagramConfig,} from 'ng-diagram';import { readOnlyMiddleware } from './read-only-middleware';
/** * Inner component that handles the diagram and controls */@Component({ selector: 'readonly-middleware-inner', imports: [NgDiagramComponent], changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './readonly-middleware-inner.component.scss', templateUrl: './readonly-middleware-inner.component.html',})export class ReadonlyMiddlewareInnerComponent { private readonly ngDiagram = inject(NgDiagramService);
isReadOnly = signal(false);
middlewares = createMiddlewares((defaults) => [ readOnlyMiddleware, ...defaults, ]);
config: NgDiagramConfig & { readOnly: any } = { zoom: { max: 3, }, readOnly: { enabled: false, allowedActions: ['changeSelection'], }, };
model = initializeModel({ nodes: [ { id: '1', position: { x: 100, y: 80 }, data: { label: 'Node 1' }, resizable: true, }, { id: '2', position: { x: 300, y: 80 }, data: { label: 'Node 2' }, resizable: true, }, { id: '3', position: { x: 200, y: 220 }, data: { label: 'Node 3' }, resizable: true, }, ], edges: [ { id: 'e1-2', source: '1', target: '2', sourcePort: 'port-right', targetPort: 'port-left', data: {}, }, { id: 'e2-3', source: '2', target: '3', sourcePort: 'port-right', targetPort: 'port-left', data: {}, }, ], metadata: { viewport: { x: 0, y: 0, scale: 1 } }, });
toggleReadOnly() { this.isReadOnly.update((current) => { const newValue = !current; this.ngDiagram.updateConfig({ readOnly: { enabled: newValue, allowedActions: ['changeSelection'], }, } as any); return newValue; }); }}
import '@angular/compiler';import { ChangeDetectionStrategy, Component } from '@angular/core';import { provideNgDiagram } from 'ng-diagram';import { ReadonlyMiddlewareInnerComponent } from './readonly-middleware-inner.component';
/** * Wrapper component that provides the ng-diagram-context */@Component({ selector: 'readonly-middleware', imports: [ReadonlyMiddlewareInnerComponent], providers: [provideNgDiagram()], changeDetection: ChangeDetectionStrategy.OnPush, template: ` <readonly-middleware-inner /> `, styleUrl: './readonly-middleware.component.scss',})export class ReadonlyMiddlewareComponent {}
<div class="diagram-container"> <div class="controls"> <button [class.active]="isReadOnly()" (click)="toggleReadOnly()" class="readonly-toggle" > {{ isReadOnly() ? '🔒 Read-Only Mode' : '✏️ Edit Mode' }} </button> <div class="status"> <span [class.readonly]="isReadOnly()"> {{ isReadOnly() ? 'Diagram is locked - try moving nodes!' : 'Diagram is editable - drag nodes around' }} </span> </div> </div> <div class="diagram"> <ng-diagram [model]="model" [config]="config" [middlewares]="middlewares" /> </div></div>
:host { display: flex; flex-direction: column; height: 100%; flex: 1;}
.diagram-container { display: flex; flex-direction: column; width: 100%; height: 25rem; border: 5px solid var(--ngd-ui-border-default); background-color: var(--ngd-ui-bg-tertiary-default);}
.controls { padding: 1rem; background: var(--sl-color-bg-nav); border-bottom: 1px solid var(--sl-color-hairline); display: flex; align-items: center; gap: 1rem; flex-shrink: 0;}
.readonly-toggle { background: var(--sl-color-accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; font-weight: 500; transition: all 0.2s ease;
&:hover { background: var(--sl-color-accent-high); }
&.active { background: var(--sl-color-red);
&:hover { background: var(--sl-color-red-high); } }}
.status { font-size: 0.875rem; color: var(--sl-color-text-accent);
.readonly { color: var(--sl-color-red); font-weight: 500; }}
.diagram { flex-grow: 1;
ng-diagram { height: 100%; width: 100%; }}
:host { display: flex; flex-direction: column; height: 100%;}
readonly-middleware-inner { display: flex; flex-direction: column; height: 100%; flex: 1;}