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 { CommonModule } from '@angular/common';import { ChangeDetectionStrategy, Component, inject } from '@angular/core';import type { NgDiagramConfig, Node } from 'ng-diagram';import { NgDiagramNodeTemplateMap, NgDiagramBackgroundComponent, NgDiagramComponent, NgDiagramModelService,} from 'ng-diagram';import { LocalStorageModelAdapter } from './local-storage-model-adapter';import { NodeComponent } from './node/node.component';
enum NodeTemplateType { CustomNodeType = 'customNodeType',}
@Component({ selector: 'diagram', imports: [CommonModule, NgDiagramComponent, NgDiagramBackgroundComponent], templateUrl: './diagram.component.html', styleUrl: './diagram.component.scss', changeDetection: ChangeDetectionStrategy.OnPush,})export class DiagramComponent { nodeTemplateMap = new NgDiagramNodeTemplateMap([ [NodeTemplateType.CustomNodeType, NodeComponent], ]); private modelService = inject(NgDiagramModelService);
config: NgDiagramConfig = { zoom: { zoomToFit: { onInit: true, padding: 180, }, }, };
modelAdapter: LocalStorageModelAdapter = new LocalStorageModelAdapter( 'ng-diagram-custom-demo', this.getDefaultDiagram() );
addNode() { const existingNodes = this.modelService.nodes(); const newId = `node-${crypto.randomUUID()}`; 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}` }, type: NodeTemplateType.CustomNodeType, };
this.modelService.addNodes([newNode]); }
reset() { if (window.confirm('Are you sure you want to reset the diagram?')) { this.resetDiagramToDefault(); } }
private resetDiagramToDefault() { const nodeIds = this.modelService.nodes().map((node) => node.id); const edgeIds = this.modelService.edges().map((edge) => edge.id); this.modelService.deleteNodes(nodeIds); this.modelService.deleteEdges(edgeIds);
const defaultDiagram = this.getDefaultDiagram(); this.modelService.addNodes(defaultDiagram.nodes); this.modelService.addEdges(defaultDiagram.edges); }
private getDefaultDiagram() { return { nodes: [ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' }, type: NodeTemplateType.CustomNodeType, }, { id: '2', position: { x: 420, y: 0 }, data: { label: 'Node 2' }, type: NodeTemplateType.CustomNodeType, }, ], edges: [ { id: 'edge-1', source: '1', target: '2', sourcePort: 'port-right', targetPort: 'port-left', data: {}, }, ], }; }}import type { Edge, Metadata, ModelAdapter, Node } from 'ng-diagram';
export class LocalStorageModelAdapter implements ModelAdapter { private callbacks: Array< (data: { nodes: Node[]; edges: Edge[]; metadata: Metadata }) => void > = []; constructor( private readonly storageKey: string = 'ng-diagram-data', initialData?: { nodes?: Node[]; edges?: Edge[]; metadata?: Metadata } ) { // 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: { nodes: Node[]; edges: Edge[]; metadata: Metadata; }) => void ): void { this.callbacks.push(callback); }
unregisterOnChange( callback: (data: { nodes: Node[]; edges: Edge[]; metadata: Metadata; }) => 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(): { nodes: Node[]; edges: Edge[]; metadata: Metadata; } { 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<{ nodes: Node[]; edges: Edge[]; metadata: Metadata }> ): 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); } }}import { Component, input } from '@angular/core';import { NgDiagramBaseNodeTemplateComponent, NgDiagramPortComponent, type NgDiagramNodeTemplate, type Node,} from 'ng-diagram';
@Component({ imports: [NgDiagramPortComponent, NgDiagramBaseNodeTemplateComponent], templateUrl: './node.component.html', styleUrls: ['./node.component.scss'], host: { '[class.ng-diagram-port-hoverable-over-node]': 'true', },})export class NodeComponent implements NgDiagramNodeTemplate { node = input.required<Node>();}<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]="modelAdapter" [config]="config" [nodeTemplateMap]="nodeTemplateMap" class="diagram" > <ng-diagram-background /> </ng-diagram> </div></div><ng-diagram-base-node-template [node]="node"> <span class="node-label">{{ node().data.label }}</span></ng-diagram-base-node-template><ng-diagram-port id="port-bottom" type="both" side="bottom" /><ng-diagram-port id="port-top" type="both" side="top" />.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; } }}:host { .node-label { width: 200px; height: 32px; display: flex; align-items: center; justify-content: center; }}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.
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.