Overview

  • How to add undo/redo functionality to the React Scheduler component.

  • The UndoService keeps a history of all drag and drop actions performed by the user. That allows unlimited undo/redo.

  • The full history of actions and the current position is displayed below the Scheduler to .

  • This tutorial assumes that you are already familiar with the React Scheduler component (for an introduction, please see the React Scheduler Component Tutorial).

  • Includes a trial version of DayPilot Pro for JavaScript (see License below)

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. Buy a license.

How to Use the UndoService to Track React Scheduler Changes

The undo/redo logic is implemented in the UndoService class. The UndoService keeps track of changes and provides an API for retrieving the last action (undo) and replaying the reverted action (redo).

First, you need to create a new instance and save it as undoService property in the React Scheduler component:

class Scheduler extends Component {

  undoService = new UndoService();

  // ...
}

Next, it’s necessary to initialize the UndoService with the initial state so it can calculate changes later. When loading events, call the initialize() method and provide the initial data set:

componentDidMount() {
  const events = [
    {
      id: 1,
      text: "Event 1",
      start: "2021-09-02T00:00:00",
      end: "2021-09-05T00:00:00",
      resource: "A"
    },

    // ...
  ];

  const resources = [
    {name: "Resource A", id: "A"},
    // ...
  ];

  // load resource and event data
  this.scheduler.update({
    events,
    resources
  });

  this.undoService.initialize(events);

}

The last step is to record all changes performed by the user. The UndoService provides add(), update(), and remove() methods. Add them to the React Scheduler event handlers:

  • onTimeRangeSelected: record a new event using UndoService.add()

  • onEventMoved: record an event change using UndoService.update()

  • onEventResized: record an event change using UndoService.update()

  • onEventDeleted: record a removed event using UndoService.remove()

The UndoService methods accept the changed object as the first parameter. Optionally, you can add a text description with details of the action.

this.state = {
  config: {

    // ...
    onTimeRangeSelected: async (args) => {
      const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
      this.scheduler.clearSelection();
      if (modal.canceled) { return; }

      const e = {
        start: args.start,
        end: args.end,
        id: DayPilot.guid(),
        resource: args.resource,
        text: modal.result
      };
      this.scheduler.events.add(e);
      this.undoService.add(e, "Event added");
    },
    onEventMoved: (args) => {
      this.undoService.update(args.e.data, "Event moved");
    },
    onEventResized: (args) => {
      this.undoService.update(args.e.data, "Event resized");
    },
    onEventDeleted: (args) => {
      this.undoService.remove(args.e.data, "Event removed");
    },
  }
};
}

How to Add Undo and Redo Buttons to the React Scheduler

react scheduler under redo buttons

The Buttons React component displays the Undo and Redo buttons.

We have wrapped the undo and redo buttons in a standalone React component. It needs to use its own state - sharing the state with the Scheduler component would trigger unnecessary updates of the Scheduler.

import React, {Component} from "react";

export class Buttons extends Component {

  constructor(props) {
    super(props);

    this.setState({
      canUndo: false,
      canRedo: false
    });

    this.props.service.watch(() => {
      this.setState({
        canUndo: this.props.service.canUndo,
        canRedo: this.props.service.canRedo
      });
    });
  }

  doUndo() {
    if (typeof this.props.onUndo === "function") {
      this.props.onUndo();
    }
  }

  doRedo() {
    if (typeof this.props.onRedo === "function") {
      this.props.onRedo();
    }
  }

  render() {
    return <div className={"space"}>
      <button disabled={!this.state?.canUndo} onClick={event => this.doUndo()}>Undo</button>
      <button disabled={!this.state?.canRedo} onClick={event => this.doRedo()}>Redo</button>
    </div>;
  }

}

The Buttons component uses three attributes:

  • service - pass the UndoService instance from the React Scheduler

  • onUndo - define the event handler for the “Undo” button click

  • onRedo - define the event handler for the “Redo” button click

<Buttons service={this.undoService} onUndo={() => this.undo()} onRedo={() => this.redo()} />

The undo() method (defined in the Scheduler component) performs the “undo” action. It reverts the last operation retrieved from the UndoService using UndoService.undo() method.

undo() {
  const action = this.undoService.undo();

  switch (action.type) {
    case "add":
      this.scheduler.events.remove(action.id);
      break;
    case "remove":
      this.scheduler.events.add(action.previous);
      break;
    case "update":
      this.scheduler.events.update(action.previous);
      break;
    default:
      throw new Error("Unexpected action type");
  }
}

The redo() method performs the “redo” action. It gets the next action from the history (it’s the last action that was reverted using the “undo” button) and replays it:

redo() {
  const action = this.undoService.redo();

  switch (action.type) {
    case "add":
      this.scheduler.events.add(action.current);
      break;
    case "remove":
      this.scheduler.events.remove(action.id);
      break;
    case "update":
      this.scheduler.events.update(action.current);
      break;
    default:
      throw new Error("Unexpected action type");
  }
}

How to Display the Undo/Redo History

react scheduler undo redo history

To show the state of the history, we’ll use a special History React component. It displays a list of changes recorded in the undo/redo history and the current position.

import React, {Component} from "react";

export class History extends Component {

  constructor(props) {
    super(props);

    this.setState({
      position: 0,
      history: []
    });

    this.props.service.watch(args => {
      this.setState({
        history: args.history,
        position: args.position
      });
    });
  }

  render() {
    const list = this.state?.history.map((item, i) => {
      const highlighted = this.state?.position === i;
      const highlightedClass = highlighted ? "highlighted" : ""
      return <div key={i} className={`history-item ${highlightedClass}`}>{item.type} - {item.text}</div>;
    });
    const total = this.state?.history.length || 0;

    return <div className={"space"}>
      <div className={"space"} style={{color: "gray"}}>There are {total} items in the history:</div>
      {list}
      <div className={this.state?.history.length && this.state.position === this.state.history.length ? "highlighted": ""}></div>
    </div>;

  }
}

To provide the History component access to the UndoService instance you need to pass it using the service attribute:

<History service={this.undoService} />