Undo and Redo in Angular Diagram Component
24 Aug 202517 minutes to read
The Angular Diagram component automatically tracks all user interactions and programmatic changes, providing robust undo and redo functionality. This feature enables users to reverse or restore actions, making diagram editing more intuitive and error-tolerant.
Prerequisites and setup
To enable undo/redo functionality in the diagram, inject the UndoRedo module into the diagram component.
NOTE
The UndoRedo module must be injected to access undo/redo features in the diagram component.
Basic undo and redo operations
The diagram provides built-in support to track changes made through both user interactions and public API calls. These changes can be reversed or restored using keyboard shortcuts or programmatic commands.
Keyboard shortcuts
Use these standard keyboard shortcuts for quick undo/redo operations:
-
Undo:
Ctrl+Z
-
Redo:
Ctrl+Y
Programmatic undo and redo
The undo
and redo
methods allow you to control undo/redo operations programmatically. The following example demonstrates how to implement these methods:
@Component({
selector: "app-container",
// Diagram template
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" (created)='created($event)'>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram: DiagramComponent;
public created(args: Object): void {
// Reverts the last action performed
this.diagram.undo();
// Restores the last undone action
this.diagram.redo();
}
}
Enabling and disabling undo/redo
Control undo/redo availability using the constraints
property of the diagram component.
History change events
The historyChange
event triggers whenever an action is undone or redone, allowing you to respond to history state changes.
Advanced history management
Grouping multiple actions
Group related changes into a single undo/redo operation using the history grouping feature. This approach allows users to undo or redo multiple related changes simultaneously rather than reversing each action individually.
Use startGroupAction
to begin grouping actions and endGroupAction
to complete the group. The following example shows how to group multiple fill color changes:
import { Component, ViewEncapsulation, ViewChild } from "@angular/core";
import { DiagramModule, UndoRedoService, DiagramComponent, Diagram, ShapeStyleModel } from "@syncfusion/ej2-angular-diagrams";
@Component({
imports: [
DiagramModule
],
providers: [UndoRedoService],
standalone: true,
selector: "app-container",
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" (created)='created($event)'>
<e-nodes>
<e-node id='node1' [offsetX]=150 [offsetY]=150 [width]=100 [height]=100>
</e-node>
</e-nodes>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
public created(args: Object): void {
//Start to group the changes
(this.diagram as Diagram).startGroupAction();
//Makes the changes
let color: string[] = ['black', 'red', 'green', 'yellow'];
for (var i = 0; i < color.length; i++) {
// Updates the fillColor for all the child elements.
((this.diagram as Diagram).nodes[0].style as ShapeStyleModel).fill = color[i];
(this.diagram as Diagram).dataBind();
}
//Ends grouping the changes
(this.diagram as Diagram).endGroupAction();
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
Managing history stack size
The stackLimit
property controls the maximum number of actions stored in the history manager. Setting an appropriate limit helps manage memory usage in applications with extensive editing operations.
import { Component, ViewEncapsulation, ViewChild } from "@angular/core";
import { DiagramModule, UndoRedoService,DiagramComponent, Diagram } from "@syncfusion/ej2-angular-diagrams";
@Component({
imports: [
DiagramModule
],
providers: [UndoRedoService],
standalone: true,
selector: "app-container",
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" (created)='created($event)'>
<e-nodes>
<e-node id='node1' [offsetX]=150 [offsetY]=150 [width]=100 [height]=100>
</e-node>
</e-nodes>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
public created(args: Object): void {
(this.diagram as Diagram).historyManager.stackLimit =3;
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
Restricting history logging
Prevent specific actions from being recorded in the history using the canLog
property. This feature is useful when certain operations should not be undoable.
import { Component, ViewEncapsulation, ViewChild } from "@angular/core";
import { DiagramModule, UndoRedoService, DiagramComponent, Diagram, HistoryEntry } from "@syncfusion/ej2-angular-diagrams";
@Component({
imports: [
DiagramModule
],
providers: [UndoRedoService],
standalone: true,
selector: "app-container",
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" (created)='created($event)'>
<e-nodes>
<e-node id='node1' [offsetX]=150 [offsetY]=150 [width]=100 [height]=100>
</e-node>
</e-nodes>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
public created(args: Object): void {
// canLog decide whether the entry add or not in history List
(this.diagram as Diagram | any).historyManager.canLog = function(entry: HistoryEntry) {
entry.cancel = true;
return entry;
}
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
History stack inspection
Accessing undo and redo stacks
The history manager provides read-only access to both undo and redo stacks through the undoStack
and redoStack
properties:
@Component({
selector: "app-container",
// render initialized Diagram
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px">
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram: DiagramComponent;
//get the collection of undoStack objects
public undoStack: HistoryEntry[] = this.diagram.historyList.undoStack;
//get the collection of redoStack objects
public redoStack: HistoryEntry[] = this.diagram.historyList.redoStack;
}
Checking availability of undo and redo operations
The canUndo
and canRedo
properties indicate whether undo or redo operations are available. These properties return true when actions exist in their respective history stacks.
import { Component, ViewEncapsulation, ViewChild } from '@angular/core';
import { DiagramModule, DiagramComponent, Diagram, UndoRedoService } from '@syncfusion/ej2-angular-diagrams';
@Component({
imports: [
DiagramModule
],
providers: [ UndoRedoService],
standalone: true,
selector: "app-container",
template: `
<ejs-diagram #diagram id="diagram" width="100%" height="580px" (historyChange)="historyChange()">
<e-nodes>
<e-node id='node1' [offsetX]=250 [offsetY]=250 [width]=100 [height]=100>
<e-node-annotations>
<e-node-annotation id="label1" content="Annotation">
</e-node-annotation>
</e-node-annotations>
</e-node>
</e-nodes>
</ejs-diagram>
<div class="button">
<button id="undo" disabled (click)='onClickUndo($event)'>Undo</button>
<button id="redo" disabled (click)='onClickRedo($event)'>Redo</button>
</div>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
onClickUndo = (args: MouseEvent) => {
(this.diagram as Diagram).undo()
}
onClickRedo = (args: MouseEvent) => {
(this.diagram as Diagram).redo()
}
public historyChange(): void {
(document.getElementById('undo') as HTMLButtonElement).disabled = !(this.diagram as Diagram).historyManager.canUndo;
(document.getElementById('redo') as HTMLButtonElement).disabled = !(this.diagram as Diagram).historyManager.canRedo;
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
Current entry tracking
During user interactions with nodes or connectors, the current history entry is stored in the currentEntry
property of the historyManager
.
import { Component, ViewEncapsulation, ViewChild } from "@angular/core";
import { DiagramModule, UndoRedoService, DiagramComponent, Diagram, IHistoryChangeArgs } from "@syncfusion/ej2-angular-diagrams";
@Component({
imports: [
DiagramModule
],
providers: [UndoRedoService],
standalone: true,
selector: "app-container",
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" (historyChange)="historyChange($event)">
<e-nodes>
<e-node id='node1' [offsetX]=150 [offsetY]=150 [width]=100 [height]=100>
</e-node>
</e-nodes>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
public historyChange(args: IHistoryChangeArgs): void {
console.log((this.diagram as Diagram | any).historyManager.currentEntry)
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
History management utilities
Clearing history
Use the clearHistory
method to remove all recorded actions from both undo and redo history stacks:
//Clears all the histories
this.diagram.clearHistory();
Retrieving history stacks
The getHistoryStack
method retrieves either the undoStack
or redoStack
from the history manager. Pass true to get the undo stack or false to get the redo stack:
// Fetch undoStack from history manager
this.diagram.getHistoryStack(true)
// Fetch redoStack from history manager
this.diagram.getHistoryStack(false)
Event handling
History change event
The historyChange
event triggers whenever interactions with nodes and connectors occur. This event provides an opportunity to implement custom logic or UI updates based on history state changes:
import { Component, ViewEncapsulation, ViewChild } from "@angular/core";
import { DiagramModule, UndoRedoService, DiagramComponent, NodeModel, IHistoryChangeArgs } from "@syncfusion/ej2-angular-diagrams";
@Component({
imports: [
DiagramModule
],
providers: [UndoRedoService],
standalone: true,
selector: "app-container",
template: `<ejs-diagram #diagram id="diagram" width="100%" height="580px" [getNodeDefaults]='getNodeDefaults' (historyChange)="historyChange($event)">
<e-nodes>
<e-node id='node1' [offsetX]=250 [offsetY]=150>
<e-node-annotations>
<e-node-annotation content="Node 1">
</e-node-annotation>
</e-node-annotations>
</e-node>
<e-node id='node2' [offsetX]=250 [offsetY]=350>
<e-node-annotations>
<e-node-annotation content="Node 2">
</e-node-annotation>
</e-node-annotations>
</e-node>
</e-nodes>
<e-connectors>
<e-connector id='connector' sourceID='node1' targetID='node2'>
</e-connector>
</e-connectors>
</ejs-diagram>`,
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild("diagram")
public diagram?: DiagramComponent;
public getNodeDefaults(node: NodeModel): NodeModel {
node.height = 100;
node.width = 100;
return node;
};
public historyChange(args: IHistoryChangeArgs): void {
console.log(args)
}
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
This event enables customization of the application behavior based on diagram interactions and history state changes.