Custom Model Example
This example demonstrates how to create a custom model implementation that persists data directly to localStorage, providing automatic persistence without keeping local copies in memory.
import '@angular/compiler';import { Component } from '@angular/core';import { provideNgDiagram } from 'ng-diagram';import { DiagramComponent } from './diagram.component';
@Component({ imports: [DiagramComponent], providers: [provideNgDiagram()], template: `<diagram />`,})export class DiagramWrapperComponent {}import { ChangeDetectionStrategy, Component, inject } from '@angular/core';import type { Model, NgDiagramConfig, Node } from 'ng-diagram';import { initializeModelAdapter, NgDiagramBackgroundComponent, NgDiagramComponent, NgDiagramModelService, NgDiagramService, NgDiagramViewportService,} from 'ng-diagram';import { LocalStorageModelAdapter } from './local-storage-model-adapter';
@Component({ selector: 'diagram', imports: [NgDiagramComponent, NgDiagramBackgroundComponent], templateUrl: './diagram.component.html', styleUrl: './diagram.component.scss', changeDetection: ChangeDetectionStrategy.OnPush,})export class DiagramComponent { private modelService = inject(NgDiagramModelService); private diagramService = inject(NgDiagramService); private viewportService = inject(NgDiagramViewportService);
config: NgDiagramConfig = { zoom: { zoomToFit: { onInit: true, padding: 20, }, }, };
model = initializeModelAdapter( new LocalStorageModelAdapter( 'ng-diagram-custom-demo', this.getDefaultModel() ) );
async addNode() { const existingNodes = this.modelService.nodes(); const newId = this.generateId(); const randomX = Math.floor(Math.random() * 400) + 50; const randomY = Math.floor(Math.random() * 300) + 50;
const newNode: Node = { id: newId, position: { x: randomX, y: randomY }, data: { label: `Custom Node ${existingNodes.length + 1}` }, };
await this.diagramService.transaction( () => { this.modelService.addNodes([newNode]); }, { waitForMeasurements: true } ); this.viewportService.zoomToFit(); }
reset() { if (window.confirm('Are you sure you want to reset the diagram?')) { this.resetDiagramToDefault(); } }
private async resetDiagramToDefault() { const nodeIds = this.modelService.nodes().map((node) => node.id); const edgeIds = this.modelService.edges().map((edge) => edge.id); const defaultModel = this.getDefaultModel();
// This works because default model IDs are unique (generated via crypto.randomUUID). // Be aware of ID collision pitfalls when mixing delete + add in a single transaction: // https://www.ngdiagram.dev/docs/guides/transactions/#how-transactions-work await this.diagramService.transaction( () => { this.modelService.deleteNodes(nodeIds); this.modelService.deleteEdges(edgeIds);
this.modelService.addNodes(defaultModel.nodes); this.modelService.addEdges(defaultModel.edges); }, { waitForMeasurements: true } ); this.viewportService.zoomToFit(); }
private generateId(): string { return crypto.randomUUID(); }
private getDefaultModel(): Model { const nodeId1 = this.generateId(); const nodeId2 = this.generateId();
return { nodes: [ { id: nodeId1, position: { x: 0, y: 0 }, data: { label: 'Node 1' }, }, { id: nodeId2, position: { x: 420, y: 0 }, data: { label: 'Node 2' }, }, ], edges: [ { id: this.generateId(), source: nodeId1, target: nodeId2, sourcePort: 'port-right', targetPort: 'port-left', data: {}, }, ], metadata: {}, }; }}import type { Edge, Metadata, Model, ModelAdapter, ModelChanges, Node,} from 'ng-diagram';
export class LocalStorageModelAdapter implements ModelAdapter { private callbacks: Array<(data: ModelChanges) => void> = []; constructor( private readonly storageKey: string = 'ng-diagram-data', initialData?: Partial<Model> ) { // Initialize storage if it doesn't exist if (!localStorage.getItem(this.storageKey)) { const defaultData = { nodes: initialData?.nodes || [], edges: initialData?.edges || [], metadata: initialData?.metadata || { viewport: { x: 0, y: 0, scale: 1 }, }, }; localStorage.setItem(this.storageKey, JSON.stringify(defaultData)); } }
// Core data access methods - read directly from localStorage getNodes(): Node[] { const data = this.getStorageData(); return data.nodes || []; }
getEdges(): Edge[] { const data = this.getStorageData(); return data.edges || []; }
getMetadata(): Metadata { const data = this.getStorageData(); return data.metadata || { viewport: { x: 0, y: 0, scale: 1 } }; }
// Data modification methods - write directly to localStorage updateNodes(next: Node[] | ((prev: Node[]) => Node[])): void { const currentNodes = this.getNodes(); const newNodes = typeof next === 'function' ? next(currentNodes) : next; this.updateStorageData({ nodes: newNodes }); this.notifyCallbacks(); }
updateEdges(next: Edge[] | ((prev: Edge[]) => Edge[])): void { const currentEdges = this.getEdges(); const newEdges = typeof next === 'function' ? next(currentEdges) : next; this.updateStorageData({ edges: newEdges }); this.notifyCallbacks(); }
updateMetadata(next: Metadata | ((prev: Metadata) => Metadata)): void { const currentMetadata = this.getMetadata(); const newMetadata = typeof next === 'function' ? next(currentMetadata) : next; this.updateStorageData({ metadata: newMetadata }); this.notifyCallbacks(); }
// Change notification system onChange(callback: (data: ModelChanges) => void): void { this.callbacks.push(callback); }
unregisterOnChange(callback: (data: ModelChanges) => void): void { this.callbacks = this.callbacks.filter((cb) => cb !== callback); }
// History management (simplified implementation) undo(): void { console.log('Undo operation - implement based on your requirements'); }
redo(): void { console.log('Redo operation - implement based on your requirements'); }
// Serialization toJSON(): string { return JSON.stringify(this.getStorageData()); }
// Cleanup destroy(): void { this.callbacks = []; }
// Private storage methods private getStorageData(): ModelChanges { try { const stored = localStorage.getItem(this.storageKey); return stored ? JSON.parse(stored) : { nodes: [], edges: [], metadata: { viewport: { x: 0, y: 0, scale: 1 } }, }; } catch (error) { console.warn('Failed to read from localStorage:', error); return { nodes: [], edges: [], metadata: { viewport: { x: 0, y: 0, scale: 1 } }, }; } }
private updateStorageData(updates: Partial<ModelChanges>): void { try { const currentData = this.getStorageData(); const newData = { ...currentData, ...updates }; localStorage.setItem(this.storageKey, JSON.stringify(newData)); } catch (error) { console.warn('Failed to save to localStorage:', error); } }
private notifyCallbacks(): void { const data = this.getStorageData();
for (const callback of this.callbacks) { callback(data); } }}<div class="example-container"> <div class="button-controls"> <button (click)="addNode()">Add Node</button> <button class="secondary" (click)="reset()">Reset to Default</button> </div>
<div class="not-content diagram"> <ng-diagram [model]="model" [config]="config" class="diagram"> <ng-diagram-background /> </ng-diagram> </div></div>.diagram { width: 100%; height: 100%; margin-top: 0;}
.example-container { display: flex; flex-direction: column; height: var(--ng-diagram-height); border: var(--ng-diagram-border); overflow: hidden; position: relative;
.button-controls { display: flex; position: absolute; right: 1rem; top: 1rem; z-index: 1; gap: 0.5rem; align-items: flex-end; justify-content: end; padding: 1rem; background-color: var(--ngd-node-bg-primary-default); border: var(--ng-diagram-border);
button { margin-top: 0; } .btn-secondary { background-color: #8b0000; color: white; } }}Additional Explanation
Section titled “Additional Explanation”Custom Model Overview
Section titled “Custom Model Overview”The NgDiagramComponent accepts any object, via the model input property (e.g. [model]="customModelAdapter"), that implements the ModelAdapter interface.
This means you can create your own custom model implementations beyond the default SignalModelAdapter provided by initializeModel.
Use initializeModelAdapter to prepare a custom adapter for use with ng-diagram.
This allows for advanced use cases like connecting to external data sources, implementing custom persistence layers, or integrating with existing state management solutions.
Key Features
Section titled “Key Features”- Direct localStorage Persistence: All data is read from and written directly to localStorage
- Single Source of Truth: No local copies of data are maintained in memory
- Automatic Synchronization: Changes are immediately persisted
- Error Handling: Robust error handling for storage operations
Implementation Details
Section titled “Implementation Details”Understanding the ModelAdapter Interface
Section titled “Understanding the ModelAdapter Interface”The ModelAdapter interface defines the contract that any model implementation must fulfill.
You can find the complete interface documentation in the API reference. The key methods include:
- Data access:
getNodes,getEdges,getMetadata - Data modification:
updateNodes,updateEdges,updateMetadata - Change notification:
onChange,unregisterOnChange - Lifecycle management:
destroy,undo,redo,toJSON
LocalStorageModelAdapter Implementation
Section titled “LocalStorageModelAdapter Implementation”This example demonstrates a custom model adapter that persists diagram data to localStorage. The data will survive page refreshes and browser sessions. You can try it by adding nodes and edges, then refreshing the page to see that your changes are retained.