Custom Model
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.
Custom Model Overview
Section titled “Custom Model Overview”The NgDiagramComponent
accepts any object that implements the ModelAdapter
interface, which 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.
import { CommonModule } from '@angular/common';import { ChangeDetectionStrategy, Component, inject } from '@angular/core';import type { Node } from 'ng-diagram';import { NgDiagramComponent, NgDiagramModelService } from 'ng-diagram';import { LocalStorageModelAdapter } from './local-storage-model-adapter';
@Component({ selector: 'app-custom-model-example', standalone: true, imports: [CommonModule, NgDiagramComponent], templateUrl: './custom-model-example.component.html', styleUrl: './custom-model-example.component.scss', changeDetection: ChangeDetectionStrategy.OnPush,})export class CustomModelExampleComponent { private modelService = inject(NgDiagramModelService);
modelAdapter: LocalStorageModelAdapter = new LocalStorageModelAdapter( 'ng-diagram-custom-demo', { nodes: [ { id: '1', position: { x: 100, y: 100 }, data: { label: 'Custom Node 1' }, }, { id: '2', position: { x: 300, y: 100 }, data: { label: 'Custom Node 2' }, }, ], edges: [ { id: 'edge-1', source: '1', target: '2', sourcePort: 'port-right', targetPort: 'port-left', data: {}, }, ], metadata: { viewport: { x: 0, y: 0, scale: 1 } }, } );
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}` }, };
this.modelService.addNodes([newNode]); }
clearAll() { 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); }}
import '@angular/compiler';import { Component } from '@angular/core';import { provideNgDiagram } from 'ng-diagram';import { CustomModelExampleComponent } from './custom-model-example.component';
@Component({ selector: 'custom-model-wrapper', imports: [CustomModelExampleComponent], providers: [provideNgDiagram()], template: ` <app-custom-model-example /> `,})export class CustomModelWrapperComponent {}
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); } }}
<div class="example-container"> <div class="button-controls"> <button class="btn primary" (click)="addNode()">Add Node</button> <button class="btn secondary" (click)="clearAll()">Clear All</button> </div>
<div class="diagram-container"> <ng-diagram [model]="modelAdapter" class="diagram"></ng-diagram> </div></div>
.example-container { display: flex; flex-direction: column; height: 600px; border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden;}
.button-controls { display: flex; gap: 1rem; padding: 1rem; background: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border); align-items: center; justify-content: center; width: 100%; margin: 0 !important;}
.btn { padding: 0.5rem 1rem; border: 1px solid var(--color-border); border-radius: 4px; background: var(--color-bg); color: var(--color-text); cursor: pointer; transition: all 0.2s;
&.primary { background: var(--color-accent); border-color: var(--color-accent);
&:hover { background: var(--color-accent-dark); } }
&:hover { background: var(--color-bg-hover); }}
.diagram-container { flex: 1; position: relative; overflow: hidden;}
.diagram { width: 100%; height: 100%;}