Skip to content

State Management

ngDiagram uses a reactive state management system that allows you to create, access, and modify diagram data efficiently. Understanding this system is essential for building dynamic, interactive diagrams.

The model is the core data structure that holds all your diagram information including nodes, edges, and metadata. Use the initializeModel function to create a new model with initial data.

import { initializeModel } from 'ng-diagram';
const model = initializeModel({
nodes: [
{
id: '1',
position: { x: 100, y: 150 },
data: { label: 'Node 1' },
},
{
id: '2',
position: { x: 400, y: 150 },
data: { label: 'Node 2' },
},
],
edges: [
{
id: 'edge-1',
source: '1',
target: '2',
data: {},
},
],
});

You can also initialize your model with metadata such as viewport settings:

const model = initializeModel({
metadata: {
viewport: { x: 100, y: 50, scale: 0.8 },
},
nodes: [
// ... your nodes
],
edges: [
// ... your edges
],
});

Despite preconfigured metadata, you can keep there any additional custom properties that you may need in your application.

For better type safety with custom data types, you can specify them when creating your model:

interface CustomNodeData {
label: string;
category: 'input' | 'output' | 'process';
}
interface CustomEdgeData {
label?: string;
style?: 'solid' | 'dashed';
}
const model = initializeModel({
nodes: [
{
id: '1',
position: { x: 100, y: 150 },
data: { label: 'Process Node', category: 'process' } as CustomNodeData,
},
// ... your nodes
],
edges: [
{
id: 'edge-1',
source: '1',
target: '2',
data: { label: 'Connection', style: 'solid' } as CustomEdgeData,
},
// ... your edges
],
});

The NgDiagramModelService provides a reactive interface to interact with your diagram’s state. It becomes available once you register provideNgDiagram() and can then be injected into your components.

import { Component, inject } from '@angular/core';
import { NgDiagramModelService } from 'ng-diagram';
@Component({
// ...
})
export class MyComponent {
private modelService = inject(NgDiagramModelService);
}

The model service provides reactive signals for nodes, edges, and metadata:

export class MyComponent {
private modelService = inject(NgDiagramModelService);
// Reactive signals
nodes = this.modelService.nodes;
edges = this.modelService.edges;
metadata = this.modelService.metadata;
ngOnInit() {
// Access current values
console.log('Current nodes:', this.nodes());
console.log('Current edges:', this.edges());
console.log('Current viewport:', this.metadata().viewport);
}
}

You can bind directly to the reactive signals in your templates:

<div>
<p>Total nodes: {{ nodes().length }}</p>
<p>Total edges: {{ edges().length }}</p>
<p>Current scale: {{ metadata().viewport.scale }}</p>
</div>

The model service provides comprehensive methods for adding, updating, deleting, and querying diagram elements.

Add new nodes and edges to your diagram:

// Add new nodes
this.modelService.addNodes([
{
id: 'new-node',
position: { x: 200, y: 200 },
data: { label: 'New Node' },
},
]);
// Add new edges
this.modelService.addEdges([
{
id: 'new-edge',
source: '1',
target: 'new-node',
sourcePort: 'port-right',
targetPort: 'port-left',
data: {},
},
]);

Update properties of existing nodes and edges:

// Update entire node
this.modelService.updateNode('node-1', {
position: { x: 300, y: 300 },
data: { label: 'Updated Node' },
});
// Update only node data
this.modelService.updateNodeData('node-1', {
label: 'New Label',
color: 'blue',
});
// Update multiple nodes at once
this.modelService.updateNodes([
{ id: '1', position: { x: 100, y: 100 } },
{ id: '2', position: { x: 200, y: 200 } },
]);
// Update edge properties
this.modelService.updateEdge('edge-1', {
data: { label: 'Updated Edge' },
});
// Update only edge data
this.modelService.updateEdgeData('edge-1', {
label: 'Connection',
style: 'dashed',
});

Remove nodes and edges from your diagram:

// Delete nodes
this.modelService.deleteNodes(['node-1', 'node-2']);
// Delete edges
this.modelService.deleteEdges(['edge-1', 'edge-2']);

Find and retrieve specific elements:

// Get element by ID
const node = this.modelService.getNodeById('node-1');
const edge = this.modelService.getEdgeById('edge-1');
// Find nearest node to a point
const nearestNode = this.modelService.getNearestNodeInRange(
{ x: 150, y: 150 },
50 // range in pixels
);
// Find nearest port to a point
const nearestPort = this.modelService.getNearestPortInRange({ x: 150, y: 150 }, 30);
// Get all nodes in a range
const nodesInRange = this.modelService.getNodesInRange({ x: 150, y: 150 }, 100);

Save and restore your diagram state:

// Export model to JSON
const jsonString = this.modelService.toJSON();
// Store in localStorage
localStorage.setItem('diagram-state', jsonString);
// Load from localStorage
const savedState = localStorage.getItem('diagram-state');
if (savedState) {
const parsedModel = JSON.parse(savedState);
// Create new model from saved state
this.model = initializeModel(parsedModel);
}

Always use the model service methods rather than directly modifying the model:

// ❌ Don't modify directly
this.model.nodes[0].position = { x: 100, y: 100 };
// ❌ Also don't use model methods directly
this.model.updateNode('node-id', { position: { x: 100, y: 100 } });
// ✅ Use model service methods
this.modelService.updateNode('node-id', {
position: { x: 100, y: 100 },
});

Leverage TypeScript for better development experience:

interface NodeData {
label: string;
color?: string;
category: 'input' | 'output' | 'process';
}
interface EdgeData {
label?: string;
style?: 'solid' | 'dashed';
}
// Type your updates
this.modelService.updateNodeData<NodeData>('node-1', {
label: 'Process Node',
category: 'process',
});
this.modelService.updateEdgeData<EdgeData>('edge-1', {
style: 'dashed',
});

For bulk operations, use batch methods when available:

// ✅ Better performance for multiple updates
this.modelService.updateNodes([
{ id: '1', position: { x: 100, y: 100 } },
{ id: '2', position: { x: 200, y: 200 } },
{ id: '3', position: { x: 300, y: 300 } },
]);
// ❌ Less efficient for multiple updates
this.modelService.updateNode('1', { position: { x: 100, y: 100 } });
this.modelService.updateNode('2', { position: { x: 200, y: 200 } });
this.modelService.updateNode('3', { position: { x: 300, y: 300 } });

The state management system provides a robust foundation for building dynamic diagrams that can respond to user interactions and external data changes while maintaining excellent performance and type safety.

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.

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 system (onChange, unregisterOnChange), and lifecycle management (destroy, undo, redo, toJSON).

For a complete example of implementing a custom model adapter, see the Custom Model example.

Custom model implementations enable several advanced scenarios:

Multi-user collaboration requires synchronizing diagram changes across different clients in real-time. A collaborative model adapter would typically integrate with WebSocket connections, services like Socket.IO or libraries like Yjs to broadcast changes and apply remote updates while handling conflict resolution.

When working with complex Angular applications that use state management libraries like NgRx or Akita, you can create model adapters that integrate seamlessly with your existing store architecture. These adapters would dispatch actions for diagram changes and subscribe to state selections, ensuring that diagram data follows your application’s established data flow patterns and benefits from features like time-travel debugging and state persistence.

class NgRxModelAdapter implements ModelAdapter {
constructor(private store: Store) {}
updateNodes(nodes: Node[]): void {
this.store.dispatch(DiagramActions.updateNodes({ nodes }));
}
onChange(callback: Function): void {
this.store.select(selectDiagramData).subscribe(callback);
}
}

Custom model implementations provide the flexibility to integrate NgDiagram with any data architecture while maintaining full compatibility with all diagram features and the model service API.