What You Will Learn in This Tutorial

  • How to add a React Scheduler component to your application

  • How to display monthly and weekly scheduler and switch between the views using zoom

  • How to load reservation data

  • How to change event/reservation color

  • How to enable drag and drop operations (moving, resizing)

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.

Generate a React Scheduler Project using an Online UI Builder App

react scheduler component configurator

To get started more easily, you can use the online UI Builder app to configure the React Scheduler component:

  1. Configure the component using the available preset values.

  2. Preview your changes immediately with a live Scheduler instance.

  3. Download a complete React project that includes your selected configuration.

Create a React Scheduler Component using 6 Lines of Code

react scheduler component initialization

To integrate a visual scheduler into your React application, the following lines of code are all you need:

import React from 'react';
import { DayPilotScheduler } from "daypilot-pro-react";

const Scheduler = () => {
    return <DayPilotScheduler />;
}

export default Scheduler;

This will render an empty Scheduler component with the default configuration, without any resources or events/reservations.

This code requires the daypilot-pro-react library which can be installed using npm:

npm install https://npm.daypilot.org/daypilot-pro-react/trial/2023.2.5582.tar.gz

You can get the latest version of daypilot-pro-react at npm.daypilot.org.

How to Load Scheduler Rows

react scheduler component resources

In order to display any useful data, we need to define rows using the resources attribute:

import React from 'react';
import { DayPilotScheduler } from "daypilot-pro-react";

const Scheduler = () => {
    return (
        <DayPilotScheduler
            resources={[
                {name: "Resource A", id: "A"},
                {name: "Resource B", id: "B"},
                {name: "Resource C", id: "C"},
                {name: "Resource D", id: "D"},
                {name: "Resource E", id: "E"},
                {name: "Resource F", id: "F"},
                {name: "Resource G", id: "G"}
            ]}
        />
    );
};

export default Scheduler;

This is an example of a React Scheduler component with statically-defined resources. In a typical React application, you will need to load the resource data from the server side using an HTTP call.

The following example uses the direct Scheduler API to load resources and display resources as rows.

  1. First, it gets a reference to Scheduler component using the ref attribute of the <DayPilotScheduler> tag. It stores the component reference in the schedulerRef mutable object, initialized using the useRef() method.

  2. Now we can access the underlying DayPilot.Scheduler object as schedulerRef.current.control.

  3. The loadData() function defines an array of resources which we load into the Scheduler component using the update() method. Depending on the specific use case, these resources could represent various entities such as rooms, machines, employees, or any other entity that requires task scheduling.

A useEffect hook is used to call loadData function after the component has been rendered. The empty dependency array ([]) passed to useEffect ensures that this side effect runs once and only once, after the initial render of the component.

import React from 'react';
import { DayPilotScheduler } from "daypilot-pro-react";

const Scheduler = () => {

    const schedulerRef = useRef();

    const loadData = (args) => {
      const resources = [
          {name: "Resource A", id: "A"},
          {name: "Resource B", id: "B"},
          {name: "Resource C", id: "C"},
          {name: "Resource D", id: "D"},
          {name: "Resource E", id: "E"},
          {name: "Resource F", id: "F"},
          {name: "Resource G", id: "G"}
      ];

      schedulerRef.current.control.update({
        resources
      });
    }

    useEffect(() => {
      loadData();
    }, []);


    return (
        <DayPilotScheduler
           ref={schedulerRef}
        />
    );
};

export default Scheduler;

How to Define the Scheduler Timeline Scale and Time Headers

react scheduler component timeline time headers

In the default configuration, the time header of the Scheduler displays today (one cell per hour). We will use startDatedaysscale and timeHeaders properties to customize the timeline:

import React from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";

const Scheduler = () => {
    return (
        <DayPilotScheduler
            startDate={DayPilot.Date.today().firstDayOfMonth()}
            days={DayPilot.Date.today().daysInMonth()}
            scale={"Day"}
            timeHeaders={[
                { groupBy: "Month" },
                { groupBy: "Day", format: "d" }
            ]}
        />
    );
};

export default Scheduler;

Our React Scheduler component now displays the current month, one cell per day.

We can also adjust the grid size using cellWidth and eventHeight properties of the React scheduler component:

<DayPilotScheduler
  cellWidth = {50}
  eventHeight = {35}
  //...
/>

The cellWidth property defines the Scheduler grid cell width in pixels. 

The eventHeight property specifies the height of event boxes. There are no events in our Scheduler yet but the height of the Scheduler rows is automatically adjusted to match the event height.

react scheduler component grid size

The React Scheduler component supports all properties from the JavaScript Scheduler API. You can use them to modify the Scheduler appearance.

So far, we have added the configuration properties to the React Scheduler directly as attributes of the <DayPilotScheduler> tag. This is convenient when using a small set of configuration properties. If you want to define more properties and change them dynamically, it is better to move them to a separate config state variable:

import React, { useState } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";

const Scheduler = () => {
    const [config, setConfig] = useState({
        startDate: DayPilot.Date.today().firstDayOfMonth(),
        days: DayPilot.Date.today().daysInMonth(),
        scale: "Day",
        timeHeaders: [
            { groupBy: "Month" },
            { groupBy: "Day", format: "d" }
        ],
    });

    return (
        <DayPilotScheduler 
           {...config}
        />
    );
};

export default Scheduler;

By using React's useState hook, the config object is made reactive - changes to this state will trigger re-render of the component. This pattern allows for more efficient and organized code, especially as the complexity of the component increases.

Just remember, whenever you want to change the value of the config object, you should use the setConfig function to ensure React's state is updated correctly and the component is re-rendered with the new configuration.

If you want to change a selected property, use the following approach:

setConfig({
  ...config, 
  startDate: config.startDate.addDays(1)
});

How to Load Reservation Data into the React Scheduler

react scheduler component loading events

In order to load events, we will extend the existing loadData() function which we used to load the Scheduler resources. The update() method lets us modify multiple properties at once, making the reload more effective.

const loadData = (args) => {
  const resources = [
    {
      name: "Convertible", id: "G2", expanded: true, children: [
        {name: "MINI Cooper", seats: 4, doors: 2, transmission: "Automatic", id: "A"},
        {name: "BMW Z4", seats: 4, doors: 2, transmission: "Automatic", id: "B"},
        {name: "Ford Mustang", seats: 4, doors: 2, transmission: "Automatic", id: "C"},
        {name: "Mercedes-Benz SL", seats: 2, doors: 2, transmission: "Automatic", id: "D"},
      ]
    },
    {
      name: "SUV", id: "G1", expanded: true, children: [
        {name: "BMW X1", seats: 5, doors: 4, transmission: "Automatic", id: "E"},
        {name: "Jeep Wrangler", seats: 5, doors: 4, transmission: "Automatic", id: "F"},
        {name: "Range Rover", seats: 5, doors: 4, transmission: "Automatic", id: "G"},
      ]
    },
  ];

  const events = [
    {id: 101, text: "Reservation 101", start: "2023-11-02T00:00:00", end: "2023-11-05T00:00:00", resource: "A"},
    {id: 102, text: "Reservation 102", start: "2023-11-06T00:00:00", end: "2023-11-10T00:00:00", resource: "A"},
    {
      id: 103,
      text: "Reservation 103",
      start: "2023-11-03T00:00:00",
      end: "2023-11-10T00:00:00",
      resource: "C",
      backColor: "#6fa8dc",
      locked: true
    },
    {
      id: 104,
      text: "Reservation 104",
      start: "2023-11-02T00:00:00",
      end: "2023-11-08T00:00:00",
      resource: "E",
      backColor: "#f6b26b",
      plus: true
    },
    {
      id: 105,
      text: "Reservation 105",
      start: "2023-11-03T00:00:00",
      end: "2023-11-09T00:00:00",
      resource: "G",
    },
    {
      id: 106,
      text: "Reservation 106",
      start: "2023-11-02T00:00:00",
      end: "2023-11-07T00:00:00",
      resource: "B",
    }
  ];

  getScheduler().update({
    resources,
    events
  });
}

We have defined the events statically using a simple events array. The items of the events array need to follow the structure defined for DayPilot.Event.data property.

In a real-world application, you can load the data from an API endpoint instead (use two parallel requests and wait for both of them to complete):

const loadData = async () => {
  try {
    const [resourcesResponse, eventsResponse] = await Promise.all([
      fetch('/api/resources'),
      fetch('/api/events'),
    ]);

    // Check that the responses are ok
    if (!resourcesResponse.ok || !eventsResponse.ok) {
      throw new Error('HTTP error, status = ' + resourcesResponse.status);
    }

    // Convert the response body to JSON
    const resources = await resourcesResponse.json();
    const events = await eventsResponse.json();

    // Update the scheduler
    setConfig((prevConfig) => ({
      ...prevConfig,
      resources,
      events,
    }));
  } catch (error) {
    console.error('Failed to fetch resources or events:', error);
  }
};

How to Switch Views using React Scheduler Zoom

react scheduler component zoom

Now that we have defined the Scheduler properties using a special state object we can use the React automatic change detection mechanism to change the Scheduler appearance dynamically.

Let's create a simple Zoom component using radio buttons that will let users choose the desired zoom level of the Scheduler:

scheduler/Zoom.js

import React, { useState } from 'react';

const Zoom = ({ onChange }) => {
    const [level, setLevel] = useState("month");

    const handleChange = (ev) => {
        const newLevel = ev.target.value;
        setLevel(newLevel);
        if (onChange) {
            onChange({level: newLevel});
        }
    };

    return (
        <span className="toolbar-item">
            Zoom:
            <label><input type="radio" name="zoom" value="month" onChange={handleChange} checked={level === "month"} /> Month</label>
            <label><input type="radio" name="zoom" value="week"  onChange={handleChange} checked={level === "week"} /> Week</label>
        </span>
    );
}

export default Zoom;

The Zoom component displays radio buttons group with two zoom level options:

  • Month

  • Week

Whenever the user selects a new value the internal component state is updated and the parent component is notified using a custom onChange callback. The onChange argument holds the newly selected zoom level:

  • args.level ("month" | "week")

Now we can add our new Zoom component to our application. We will listen to zoom level changes by defining an onChange event handler. Our event handler updates the required properties of the config object. The Scheduler component detects a state change and updates automatically.

import React, { useState } from 'react';
import { DayPilot, DayPilotScheduler } from "daypilot-pro-react";
import Zoom from "./Zoom";

const Scheduler = () => {
    const [config, setConfig] = useState({
        startDate: "2023-10-01",
        days: 31,
        scale: "Day",
        timeHeaders: [
            { groupBy: "Month" },
            { groupBy: "Day", format: "d" }
        ],
        // ...
    });

    const zoomChange = (args) => {
        switch (args.level) {
            case "month":
                setConfig({
                    ...config,
                    startDate: DayPilot.Date.today().firstDayOfMonth(),
                    days: DayPilot.Date.today().daysInMonth(),
                    scale: "Day"
                });
                break;
            case "week":
                setConfig({
                    ...config,
                    startDate: DayPilot.Date.today().firstDayOfWeek(),
                    days: 7,
                    scale: "Day"
                });
                break;
            default:
                throw new Error("Invalid zoom level");
        }
    }

    return (
        <div>
            <div className="toolbar">
                <Zoom onChange={zoomChange} />
            </div>
            <DayPilotScheduler {...config} />
        </div>
    );
}

export default Scheduler;

How to Handle User Events (Drag and Drop)

react scheduler component event moving drag and drop

The Scheduler defines event handlers that you can use to get notifications of user changes. Let's see how it works for drag and drop event moving.

Simply define a callback method that receives an args object as a parameter. The args object holds details about the drag and drop action and you can use it to notify the server about the change:

<DayPilotScheduler
    // ...
    onEventMoved={args => {
      console.log("Event moved: ", args.e.data.id, args.newStart, args.newEnd, args.newResource);
    }}
/>

How to Resize Scheduler Events using Drag and Drop

react scheduler component drag and drop resizing

You can handle drag and drop event resizing using the same mechanism. The Scheduler events/reservations can be resized by dragging either the start or end. As soon as the user finishes the resizing the Scheduler fires onEventResized event.

The resizing is enabled by default (eventResizeHandling is set to "Update") and all we need to do is to add a new onEventResized event handler:

<DayPilotScheduler
  // ...
  onEventResized={args => {
      console.log("Event resized: ", args.e.data.id, args.newStart, args.newEnd);
      schedulerRef.current.control.message("Event resized: " + args.e.data.text);
  }}
/>

How to Add New Events/Reservations

react scheduler component creating events

To add support for creating new events, we need to handle onTimeRangeSelected event handler:

<DayPilotScheduler
  // ...
  onTimeRangeSelected={async (args) => {
    const modal = await DayPilot.Modal.prompt("New event name", "Event");
    schedulerRef.current.control.clearSelection();
    if (modal.canceled) {
      return;
    }
    schedulerRef.current.control.events.add({
      id: DayPilot.guid(),
      text: modal.result,
      start: args.start,
      end: args.end,
      resource: args.resource
    });
  }}
/>

The event handler will open a simple prompt using the built-in DayPilot.Modal dialog and ask for the event name. When the user hits OK, we create the event using the direct API - events.add() method.

react scheduler component new event modal dialog

How to Display a Message to the User using React Scheduler API

react scheduler component message direct api

The Scheduler component offers a rich API which you can use in your application. The Scheduler methods let you use advanced features that can't be controlled by properties.

The following example uses message() method to display a message at the top of the Scheduler. It fades away after a couple of seconds and it can be used to provide feedback to users.

As we mentioned before, we need to get a reference to the DayPilot.Scheduler object to call its methods:


const schedulerRef = useRef();

// ...


return (
    <DayPilotScheduler
      // ...
      ref={schedulerRef}
    />
);

Now we can use schedulerRef.current.control to invoke the Scheduler methods directly:

<DayPilotScheduler
    // ...
    onEventMoved={args => schedulerRef.current.control.message("Event moved: " + args.e.data.text)}
    // ...
/>

To make the access to the DayPilot.Scheduler object easier, we can add a shortcut function:

const getScheduler = () => schedulerRef.current.control;

How to Customize the Color of Reservations

react scheduler component tutorial reservation color

The React Scheduler component is very customizable and it lets you change the event color easily.

By default, the events are white and display a colored bar at the top. If you want to hide the bar, you need to add durationBarVisible property to the config:

const [config, setConfig] = useState({
  durationBarVisible: false,
  // ...other fields...
});

To change the event color depending on the event type, you need to use onBeforeEventRender event handler. This event is fired for every event/reservation when it is loaded.

onBeforeEventRender: args => {
  if (!args.data.backColor) {
    args.data.backColor = "#93c47d";
  }
  args.data.borderColor = "darker";
  args.data.fontColor = "white";
}

The "darker" value of the borderColor property is a built-in helper that will calculate a darker color from backColor value automatically.

Full Source Code of Scheduler.js

scheduler/Scheduler.js

import React, {useState, useRef, useEffect} from 'react';
import {DayPilot, DayPilotScheduler} from "daypilot-pro-react";
import Zoom from "./Zoom";

const Scheduler = () => {
  const [config, setConfig] = useState({

    startDate: "2023-11-01",
    days: 31,
    scale: "Day",
    timeHeaders: [
      {groupBy: "Month"},
      {groupBy: "Day", format: "d"}
    ],
    cellWidthSpec: "Auto",
    cellWidth: 50,
    durationBarVisible: false,
    treeEnabled: true,
    rowHeaderColumns: [
      {name: "Car"},
      {name: "Seats", display: "seats", width: 50},
      {name: "Doors", display: "doors", width: 50},
      {name: "Transmission", display: "transmission", width: 90},
    ],
    onEventMoved: args => {
      console.log("Event moved: ", args.e.data.id, args.newStart, args.newEnd, args.newResource);
      getScheduler().message("Event moved: " + args.e.data.text);
    },
    onEventResized: args => {
      console.log("Event resized: ", args.e.data.id, args.newStart, args.newEnd);
      getScheduler().message("Event resized: " + args.e.data.text);
    },
    onTimeRangeSelected: args => {
      DayPilot.Modal.prompt("New event name", "Event").then(modal => {
        getScheduler().clearSelection();
        if (!modal.result) {
          return;
        }
        getScheduler().events.add({
          id: DayPilot.guid(),
          text: modal.result,
          start: args.start,
          end: args.end,
          resource: args.resource
        });
      });
    },
    onBeforeEventRender: args => {
      if (!args.data.backColor) {
        args.data.backColor = "#93c47d";
      }
      args.data.borderColor = "darker";
      args.data.fontColor = "white";

      args.data.areas = [];
      if (args.data.locked) {
        args.data.areas.push(
          {
            right: 4,
            top: 8,
            height: 18,
            width: 18,
            symbol: "icons/daypilot.svg#padlock",
            fontColor: "white"
          }
        );
      } else if (args.data.plus) {
        args.data.areas.push(
          {
            right: 4,
            top: 8,
            height: 18,
            width: 18,
            symbol: "icons/daypilot.svg#plus-4",
            fontColor: "white"
          }
        );
      }
    },
  });

  const schedulerRef = useRef();

  const getScheduler = () => schedulerRef.current.control;

  const zoomChange = (args) => {
    switch (args.level) {
      case "month":
        setConfig({
          ...config,
          startDate: DayPilot.Date.today().firstDayOfMonth(),
          days: DayPilot.Date.today().daysInMonth(),
          scale: "Day"
        });
        break;
      case "week":
        setConfig({
          ...config,
          startDate: DayPilot.Date.today().firstDayOfWeek(),
          days: 7,
          scale: "Day"
        });
        break;
      default:
        throw new Error("Invalid zoom level");
    }
  };

  const cellWidthChange = (ev) => {
    const checked = ev.target.checked;
    setConfig(prevConfig => ({
      ...prevConfig,
      cellWidthSpec: checked ? "Auto" : "Fixed"
    }));
  };

  const loadData = (args) => {
    const resources = [
      {
        name: "Convertible", id: "G2", expanded: true, children: [
          {name: "MINI Cooper", seats: 4, doors: 2, transmission: "Automatic", id: "A"},
          {name: "BMW Z4", seats: 4, doors: 2, transmission: "Automatic", id: "B"},
          {name: "Ford Mustang", seats: 4, doors: 2, transmission: "Automatic", id: "C"},
          {name: "Mercedes-Benz SL", seats: 2, doors: 2, transmission: "Automatic", id: "D"},
        ]
      },
      {
        name: "SUV", id: "G1", expanded: true, children: [
          {name: "BMW X1", seats: 5, doors: 4, transmission: "Automatic", id: "E"},
          {name: "Jeep Wrangler", seats: 5, doors: 4, transmission: "Automatic", id: "F"},
          {name: "Range Rover", seats: 5, doors: 4, transmission: "Automatic", id: "G"},
        ]
      },
    ];

    const events = [
      {id: 101, text: "Reservation 101", start: "2023-11-02T00:00:00", end: "2023-11-05T00:00:00", resource: "A"},
      {id: 102, text: "Reservation 102", start: "2023-11-06T00:00:00", end: "2023-11-10T00:00:00", resource: "A"},
      {
        id: 103,
        text: "Reservation 103",
        start: "2023-11-03T00:00:00",
        end: "2023-11-10T00:00:00",
        resource: "C",
        backColor: "#6fa8dc",
        locked: true
      },
      {
        id: 104,
        text: "Reservation 104",
        start: "2023-11-02T00:00:00",
        end: "2023-11-08T00:00:00",
        resource: "E",
        backColor: "#f6b26b",
        plus: true
      },
      {
        id: 105,
        text: "Reservation 105",
        start: "2023-11-03T00:00:00",
        end: "2023-11-09T00:00:00",
        resource: "G",
      },
      {
        id: 106,
        text: "Reservation 106",
        start: "2023-11-02T00:00:00",
        end: "2023-11-07T00:00:00",
        resource: "B",
      }
    ];

    getScheduler().update({
      resources,
      events
    });
  }

  useEffect(() => {
    loadData();
  }, []);

  return (
    <div>
      <div className="toolbar">
        <Zoom onChange={args => zoomChange(args)}/>
        <button onClick={ev => getScheduler().message("Welcome!")}>Welcome!</button>
        <span className="toolbar-item"><label><input type="checkbox" checked={config.cellWidthSpec === "Auto"}
                                                     onChange={ev => cellWidthChange(ev)}/> Auto width</label></span>
      </div>
      <DayPilotScheduler
        {...config}
        ref={schedulerRef}
      />
    </div>
  );
}

export default Scheduler;

Main React Application Class

App.js

import React from 'react';
import './App.css';
import Scheduler from "./scheduler/Scheduler";

const App = () => {
  return (
    <Scheduler/>
  );
}

export default App;

History

  • May 26, 2023: Converted to functional React component (Hooks API), React 18, DayPilot Pro for JavaScript 2023.2.5582.

  • June 21, 2021: Row columns added, how to change event color.

  • May 20, 2021: Upgraded to React 17, DayPilot Pro for JavaScript 2021.2.4990.

  • November 22, 2020: Upgraded to DayPilot Pro for JavaScript 2020.4.4766.

  • September 11, 2019: Upgraded to DayPilot Pro for JavaScript 2019.3.4000, toolbar styling, event creating, event resizing.

  • May 21, 2018: Component interop (zoom component), custom event bar color.

  • May 18, 2018: Initial version released.