Save Persistence
This example demonstrates how to implement save to the diagram in ngDiagram using Angular features.
Save Persistence Overview
Section titled “Save Persistence Overview”The save functionality allows users to persist the current diagram (nodes and edges) in local storage, restore it later, or clear the saved state.
Key Features
Section titled “Key Features”- Persistence: Saves the diagram state to local storage.
- Restoration: Loads the saved state back into the diagram.
- Clear: Removes the saved state from local storage.
Implementation Details
Section titled “Implementation Details”Save Persistence Service
Section titled “Save Persistence Service”The SaveStateService
manages saving, loading, and clearing the diagram state using Angular signals and effects:
8 collapsed lines
import { effect, Injectable, signal } from '@angular/core';import { type Edge, type Metadata, type Node } from 'ng-diagram';
@Injectable()export class SaveStateService { private KEY = 'ngDiagramSaveStateKey';
state = signal<string | null>(localStorage.getItem(this.KEY)); stateSync = effect(() => { if (this.state() === null) { localStorage.removeItem(this.KEY); } else { localStorage.setItem(this.KEY, this.state() as string); } });
save(newState: string): void { this.state.set(newState); }
load(): { nodes: Node[]; edges: Edge[]; metadata: Metadata } | null { try { const serializedState = this.state(); return serializedState ? JSON.parse(serializedState) : null; } catch (e) { console.error('SaveStateService: Error loading state', e); return null; } }
clear(): void { this.state.set(null); }}
NavBar Component
Section titled “NavBar Component”The navigation bar provides buttons for saving, loading, and clearing the diagram state. Button states are reactive:
4 collapsed lines
import { Component, computed, inject, output, signal } from '@angular/core';import { NgDiagramModelService } from 'ng-diagram';import { SaveStateService } from '../save.service';
@Component({ selector: 'nav-bar', template: ` <div class="nav-bar"> <div class="status">{{ statusMessage() }}</div> <button (click)="save()">Save</button> <button (click)="load()" [disabled]="!isSaved()">Load</button> <button class="clear" (click)="clear()" [disabled]="!isSaved()"> Clear </button> </div> `, styleUrls: ['./nav-bar.component.scss'],})export class NavBarComponent {42 collapsed lines
//TODO - change any type to the correct one loadModel = output<any>();
private readonly saveStateService = inject(SaveStateService); private readonly modelService = inject(NgDiagramModelService); isSaved = computed(() => { return this.saveStateService.state() !== null; });
private statusMessage = signal(''); private statusTimeout: ReturnType<typeof setTimeout> | null = null;
save(): void { const model = this.modelService.toJSON(); this.saveStateService.save(model); this.showStatus('State has been successfully saved!'); }
load(): void { const model = this.saveStateService.load();
if (model) { this.loadModel.emit(model); this.showStatus('State has been loaded!'); } }
clear(): void { if (window.confirm('Are you sure you want to clear the saved state?')) { this.saveStateService.clear(); this.showStatus('State has been cleared!'); } }
showStatus(msg: string): void { this.statusMessage.set(msg);
if (this.statusTimeout) { clearTimeout(this.statusTimeout); } this.statusTimeout = setTimeout(() => { this.statusMessage.set(''); this.statusTimeout = null; }, 6000);
Component Structure
Section titled “Component Structure”The example wraps the diagram and navigation bar in a context provider, ensuring services are available.
Actions
Section titled “Actions”- Save: Serializes and stores the current diagram state.
- Load: Restores the diagram from the saved state.
- Clear: Removes the saved state from local
import { Component, computed, inject, output, signal } from '@angular/core';import { NgDiagramModelService } from 'ng-diagram';import { SaveStateService } from '../save.service';
@Component({ selector: 'nav-bar', template: ` <div class="nav-bar"> <div class="status">{{ statusMessage() }}</div> <button (click)="save()">Save</button> <button (click)="load()" [disabled]="!isSaved()">Load</button> <button class="clear" (click)="clear()" [disabled]="!isSaved()"> Clear </button> </div> `, styleUrls: ['./nav-bar.component.scss'],})export class NavBarComponent { //TODO - change any type to the correct one loadModel = output<any>();
private readonly saveStateService = inject(SaveStateService); private readonly modelService = inject(NgDiagramModelService); isSaved = computed(() => { return this.saveStateService.state() !== null; });
private statusMessage = signal(''); private statusTimeout: ReturnType<typeof setTimeout> | null = null;
save(): void { const model = this.modelService.toJSON(); this.saveStateService.save(model); this.showStatus('State has been successfully saved!'); }
load(): void { const model = this.saveStateService.load();
if (model) { this.loadModel.emit(model); this.showStatus('State has been loaded!'); } }
clear(): void { if (window.confirm('Are you sure you want to clear the saved state?')) { this.saveStateService.clear(); this.showStatus('State has been cleared!'); } }
showStatus(msg: string): void { this.statusMessage.set(msg);
if (this.statusTimeout) { clearTimeout(this.statusTimeout); } this.statusTimeout = setTimeout(() => { this.statusMessage.set(''); this.statusTimeout = null; }, 6000); }}
import '@angular/compiler';import { Component, inject, Injector } from '@angular/core';import { initializeModel, NgDiagramComponent, NgDiagramModelService, type NgDiagramConfig,} from 'ng-diagram';import { NavBarComponent } from './nav-bar/nav-bar.component';
@Component({ selector: 'save-state-example', imports: [NgDiagramComponent, NavBarComponent], template: ` <div> <nav-bar (loadModel)="loadModel($event)"></nav-bar> <div class="not-content diagram"> <ng-diagram [model]="model" [config]="config" /> </div> </div> `, styleUrl: './save-example.component.scss', providers: [NgDiagramModelService],})export class SaveStateExampleComponent { private injector = inject(Injector); config = { zoom: { max: 3, }, } satisfies NgDiagramConfig;
model = initializeModel({ nodes: [ { id: '1', position: { x: 100, y: 100 }, data: {}, }, { id: '2', position: { x: 200, y: 150 }, data: {}, }, { id: '3', position: { x: 300, y: 200 }, data: {}, }, ], edges: [], metadata: { viewport: { x: 0, y: 0, scale: 1 } }, });
// TODO - change any type to the correct one loadModel(model: any): void { this.model = initializeModel(model, this.injector); }}
import '@angular/compiler';import { Component } from '@angular/core';import { NgDiagramModelService, provideNgDiagram } from 'ng-diagram';import { SaveStateExampleComponent } from './save-example.component';import { SaveStateService } from './save.service';
@Component({ selector: 'save-state-wrapper', imports: [SaveStateExampleComponent], template: ` <save-state-example></save-state-example> `, styles: [ ` :host { flex: 1; display: flex; position: relative; flex-direction: column; } `, ], providers: [NgDiagramModelService, SaveStateService, provideNgDiagram()],})export class SaveStateWrapperComponent {}
import { effect, Injectable, signal } from '@angular/core';import { type Edge, type Metadata, type Node } from 'ng-diagram';
@Injectable()export class SaveStateService { private KEY = 'ngDiagramSaveStateKey';
state = signal<string | null>(localStorage.getItem(this.KEY)); stateSync = effect(() => { if (this.state() === null) { localStorage.removeItem(this.KEY); } else { localStorage.setItem(this.KEY, this.state() as string); } });
save(newState: string): void { this.state.set(newState); }
load(): { nodes: Node[]; edges: Edge[]; metadata: Metadata } | null { try { const serializedState = this.state(); return serializedState ? JSON.parse(serializedState) : null; } catch (e) { console.error('SaveStateService: Error loading state', e); return null; } }
clear(): void { this.state.set(null); }}
.nav-bar { display: flex; gap: 8px; align-items: flex-end; justify-content: end; padding: 2px 16px;
& .status { flex: 1; background: var(--ngd-node-stroke-primary-default); padding: 2px 12px;
&:empty { background: none; } }
& > button { cursor: pointer; border: none;
&.clear { background: darkred; }
&:disabled { cursor: not-allowed;
&.clear { background: none; } } }}
:host { width: 100%; height: 100%;}
.diagram { flex: 1; display: flex; height: 20rem; font-family: 'Poppins', sans-serif;}