Features

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.

Project Initialization

In order to download the dependencies (node_modules) it's necessary to initialize the project using npm:

npm install

Running the Angular Project

You can run the project using npm:

npm run start

This command will start an embedded web server at http://localhost:4200.

Angular Scheduler Configuration

angular scheduler undo redo basic configuration

The installation and configuration of the Angular Scheduler component is not covered by this tutorial. For an introduction and a boilerplate Angular project please see the following tutorial:

Scheduler Undo Service API

angular scheduler undo service

This project includes UndoService class that provides undo/redo queue for tracking event/appointment changes in the Scheduler control. 

However, the implementation is generic and can be used for any kind of object that meets the following conditions:

  • the object has an id property with type of number or string

  • the objects is serializable using JSON.stringify()

initialize(items: Item[]): void

Initializes the UndoService instance with the initial state of all events. It's necessary to initialize the Undo service before registering user actions.

add(item: Item, text?: string): HistoryItem

Registers an "add" action.

update(item: Item, text?: string): HistoryItem

Registers an "update" action.

remove(item: Item, text?: string): HistoryItem

Registers a "remove" action.

get canUndo(): boolean

Returns true if "undo" is possible. You can use this property to enable/disable the Undo button.

get canRedo(): boolean

Returns true if "redo" is possible. You can use this property to enable/disable the Redo button.

undo(): HistoryItem

Performs "undo" on the internal state and returns HistoryItem object with action details so the action can be reverted.

redo(): HistoryItem

Performs "redo" on the internal state and returns HistoryItem object with actions details so the action can be replayed.

get history(): HistoryItem[]

Returns the full undo/redo queue with action details.

get position(): number

Gets the current position in the undo/redo queue.

Using Undo Service with Angular Scheduler Component

angular scheduler undo redo initialization

When starting with a project generated using DayPilot UI Builder we already have a configured Scheduler component (SchedulerComponent in src/app/scheduler/scheduler.component.ts). Now we can modify this component to add the undo/redo functionality.

1. First, we need an instance of UndoService:

export class SchedulerComponent implements AfterViewInit {

  constructor(/*... */, public us: UndoService) {
    console.log("UndoService injected as this.us");
  }

  // ...

}

2. The UndoService needs to be initialized with the initial event set. As soon as we receive the event array from the DataService we load the events in the Scheduler and initialize the UndoService:

export class SchedulerComponent implements AfterViewInit {

  ngAfterViewInit(): void {
    // ...

    var from = this.scheduler.control.visibleStart();
    var to = this.scheduler.control.visibleEnd();
    this.ds.getEvents(from, to).subscribe(result => {
      this.events = result;
      this.us.initialize(this.events);
    });
  }

  // ...

}

3. Now we need to register all changes made to the events. We will do this by adding the UndoService action registration calls to the Scheduler events handlers (onTimeRangeSelected, onEventMoved, onEventResized, onEventDeleted):

export class SchedulerComponent implements AfterViewInit {

  // ...

  config: SchedulerConfig = {
    
    // ...

    timeRangeSelectedHandling: "Enabled",
    onTimeRangeSelected: args => {
      let component = this;
      DayPilot.Modal.prompt("Create a new event:", "Event 1").then(function(modal) {
        let dp = args.control;
        dp.clearSelection();
        if (!modal.result) { return; }
        let data = {
          start: args.start,
          end: args.end,
          id: DayPilot.guid(),
          resource: args.resource,
          text: modal.result
        };
        dp.events.add(new DayPilot.Event(data));
        component.us.add(data, "Event created.");
      });
    },

    onEventMoved: args => {
      this.us.update(args.e.data, "Event moved.");
    },
    onEventResized: args => {
      this.us.update(args.e.data, "Event resized.");
    },

    eventDeleteHandling: "Update",
    onEventDeleted: args => {
      this.us.remove(args.e.data, "Event deleted.");
    }
  };

  // ...

}

When calling the registration methods [add(), update(), remove()] we provide the new event state (that's why we are using event handlers that are called after the default action, e.g. onEventMoved instead of onEventMove) and also a text that describes the action. The text is stored in the history and we can use it to provide more details to users.

As the next steps, we will add the Undo and Redo buttons and History to the template.

How to Add “Undo” Button

angular scheduler undo button

HTML Template

<button (click)="undo()" [disabled]="!us.canUndo">Undo</button>

TypeScript

undo(): void {
  let item: HistoryItem = this.us.undo();

  switch (item.type) {
    case "add":
      // added, need to delete now
      this.scheduler.control.events.remove(item.id);
      break;
    case "remove":
      // removed, need to add now
      this.scheduler.control.events.add(<EventData> item.previous);
      break;
    case "update":
      // updated
      this.scheduler.control.events.update(<EventData> item.previous);
      break;
  }

}

The undo() method calls the UndoService.undo() method to update the internal state and to get information about the last action.

The HistoryItem object holds information about the event id, a copy of the previous object state, a copy of the new object state. We use the previous state to revert the change.

How to Add “Redo” Button

angular scheduler redo button

HTML Template

<button (click)="redo()" [disabled]="!us.canRedo">Redo</button>

TypeScript

redo(): void {
  let item: HistoryItem = this.us.redo();

  switch (item.type) {
    case "add":
      // added, need to re-add
      this.scheduler.control.events.add(<EventData> item.current);
      break;
    case "remove":
      // removed, need to remove again
      this.scheduler.control.events.remove(item.id);
      break;
    case "update":
      // updated, use the new version
      this.scheduler.control.events.update(<EventData> item.current);
      break;
  }

}

The redo() method calls the UndoService.redo() method to update the internal state and to get information about the last undo.

The HistoryItem object holds information about the event id, a copy of the previous object state, a copy of the new object state. We use the new object state to replay the change.

Scheduler Undo/Redo History

angular scheduler undo redo history

As a visual hint, this project also displays the current undo/redo queue and the current position.

HTML

<h2>History</h2>
<div *ngFor="let item of us.history; let i = index" [class.highlighted]="i === us.position" class="history-item">{{item.time}}: {{item.text}} (ID: {{item.id}})</div>
<div [class.highlighted]="us.history.length && us.position === us.history.length"></div>

CSS

.history-item {
  margin-bottom: 10px;
}

.highlighted {
  position: relative;
}

.highlighted::before {
  content: "position";
  position: absolute;
  width: 60px;
  height: 1px;
  top: -10px;
  border-top: 2px solid red;
  color: red;
  font-size: 8px;
}

Undo Service Source Code

Here is the source code of the UndoService class which provides the undo/redo functionality for the Scheduler component.

undo.service.ts

import {Injectable} from '@angular/core';
import {DayPilot} from "daypilot-pro-angular";

@Injectable()
export class UndoService {

  private _items: any;

  private _history: HistoryItem[] = [];

  get history(): HistoryItem[] {
    return this._history;
  }

  private _position: number = 0;

  get position(): number {
    return this._position;
  }

  get canUndo(): boolean {
    return this._position > 0;
  }

  get canRedo(): boolean {
    return this._position < this._history.length;
  }

  initialize(items: Item[]): void {
    // deep copy using JSON serialization/deserialization
    this._items = [];
    items.forEach(i => {
      let str = JSON.stringify(i);
      let key = this.keyForItem(i);
      if (this._items[key]) {
        throw "Duplicate IDs are not allowed.";
      }
      this._items[key] = str;
    });

    this._history = [];
  }

  update(item: Item, text?: string): HistoryItem {
    let key = this.keyForItem(item);
    let stringified = JSON.stringify(item);
    if (!this._items[key]) {
      throw "The item to be updated was not found in the list.";
    }
    if (this._items[key] === stringified) {
      throw "The item to be updated has not been modified.";
    }
    let record: HistoryItem = {
      id: item.id,
      time: new DayPilot.Date(),
      previous: JSON.parse(this._items[key]),
      current: JSON.parse(stringified),
      text: text || "",
      type: "update"
    };

    this._items[key] = stringified;
    this.addToHistory(record);

    return record;
  }

  add(item: Item, text?: string): HistoryItem {
    let key = this.keyForItem(item);
    if (this._items[key]) {
      throw "Item is already in the list";
    }
    let record: HistoryItem = {
      id: item.id,
      time: new DayPilot.Date(),
      previous: null,
      current: item,
      text: text || "",
      type: "add"
    };

    this._items[key] = JSON.stringify(item);
    this.addToHistory(record);

    return record;
  }

  remove(item: Item, text?: string): HistoryItem {
    let key = this.keyForItem(item);
    if (!this._items[key]) {
      throw "The item to be removed was not found in the list.";
    }
    if (this._items[key] !== JSON.stringify(item)) {
      throw "The item to be removed has been modified.";
    }
    let record: HistoryItem = {
      id: item.id,
      time: new DayPilot.Date(),
      previous: item,
      current: null,
      text: text || "",
      type: "remove"
    };

    this._items[key] = null;
    this.addToHistory(record);

    return record;
  }

  undo(): HistoryItem {
    if (!this.canUndo) {
      throw "Can't undo";
    }

    this._position -= 1;
    let record = this._history[this._position];

    let key = this.keyForId(record.id);
    switch (record.type) {
      case "add":
        this._items[key] = null;
        break;
      case "remove":
        this._items[key] = JSON.stringify(record.previous);
        break;
      case "update":
        this._items[key] = JSON.stringify(record.previous);
        break;
    }

    return record;
  }

  redo(): HistoryItem {
    if (!this.canRedo) {
      throw "Can't redo";
    }


    let record = this._history[this._position];
    this._position += 1;

    let key = this.keyForId(record.id);
    switch (record.type) {
      case "add":
        this._items[key] = JSON.stringify(record.current);
        break;
      case "remove":
        this._items[key] = null;
        break;
      case "update":
        this._items[key] = JSON.stringify(record.current);
        break;
    }

    return record;
  }

  private keyForItem(item: Item): string {
    return this.keyForId(item.id);
  }

  private keyForId(id: string | number): string {
    return "_" + id;
  }

  private addToHistory(record: HistoryItem): void {
    while (this.canRedo) {
      this._history.pop();
    }
    this._history.push(record);
    this._position += 1;
  }

}

export interface HistoryItem {
  id: string | number;
  time: DayPilot.Date;
  previous: Item | null;
  current: Item | null;
  text: string;
  type: string;
  active?: boolean;
}

export interface Item {
  id: string | number;
}