Arsalan Khattak
24 May 2023

Migrate from FullCalendar to Bryntum Calendar and Scheduler

This tutorial will walk you through the process of transferring your current scheduling tasks from FullCalendar to Bryntum Calendar. Bryntum Calendar is a […]

This tutorial will walk you through the process of transferring your current scheduling tasks from FullCalendar to Bryntum Calendar. Bryntum Calendar is a commercial project management tool with more built-in features and advanced customization options than FullCalendar. Whether you’re looking for a change or want to explore Bryntum Calendar, we’ve got you covered.

We’ll take you through these steps to get your Bryntum Calendar set up and move your FullCalendar data to MySQL tables compatible with Bryntum Calendar:

  1. Installing and configuring Bryntum Calendar.
  2. Establishing API endpoints for the server.
  3. Creating database tables that are compatible with Bryntum Calendar.
  4. Testing the migration process.

After this migration, we’ll adjust the code to show you how to do a similar migration from FullCalendar with a timeline view, which we’ll refer to as FullCalendar Scheduler, to Bryntum Scheduler. The schedulers are calendars that have resources associated with each event.

Getting started

To demonstrate the process of migrating to Bryntum Calendar, we will use an existing starter project for FullCalendar that uses JavaScript on the client side and Node.js with REST API endpoints on the server side. Depending on your implementation and the client-side technology you use (such as React or Angular), your code may differ slightly from ours.

If you prefer to follow along with our starter project instead of updating your own project code, you can clone the FullCalendar starter GitHub repository. Please refer to the README.md file for instructions on setting up the starter project.

Once you have completed the migration process, Bryntum Calendar should display the same data as the original FullCalendar app.

Install and set up Bryntum Calendar

We will create a new folder for our Bryntum Calendar project.

In the root directory of your project, run the following command to make the files necessary for the Bryntum Calendar:

mkdir -p BryntumCalendar/public && touch BryntumCalendar/{server.js,package.json,.env} BryntumCalendar/public/{index.html,main.js,main.css} && cd BryntumCalendar

Populate the .env file with the same contents as the .env you created for the FullCalendar app. It should look something like this:

DB_HOST='localhost'
DB_USER='root'
DB_PASSWORD='<your-password>'
DB_DATABASE='fullcalendar'
PORT=3000

Next, populate the package.json file we just created with the following:

{
    "name": "bryntumcalendar",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "type": "module",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "nodemon server.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "body-parser": "^1.20.2",
        "cors": "^2.8.5",
        "dotenv": "^16.3.1",
        "express": "^4.18.2",
        "mysql2": "^3.6.1"
    },
    "devDependencies": {
        "nodemon": "^3.0.1"
    }
}

Run npm install to install the appropriate packages for the project.

Set up the client side

Install Bryntum Calendar by following step 1 and step 4 of the vanilla JavaScript with npm set-up guide.

Open the public/index.html file and add the following code to link to the Bryntum stylesheets and give our calendar some additional styling:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bryntum Calendar with MySQL</title>
    <link rel="stylesheet" href="./calendar.stockholm.css" id="bryntum-theme" />
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <script type="module" src="main.js"></script>
  </body>
</html>

Next, we will style the Bryntum Calendar component. Add the following code inside public/main.css:

body,
html {
  margin: 0;
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: Poppins, 'Open Sans', Helvetica, Arial, sans-serif;
  font-size: 14px;
}

Now we will set up the Bryntum Calendar component. Add the following code to public/main.js:

import { Calendar } from "./calendar.module.js";
const calendar = new Calendar({
  appendTo: document.body,
  crudManager: {
    autoLoad: true,
    autoSync: true,
    transport: {
      load: {
        url: "http://localhost:3000/api/events",
      },
      sync: {
        url: "http://localhost:3000/api/events",
      },
    },
  },
});

We have now imported the Bryntum Calendar component from the library and set it up to load data from the /api/events endpoint on the initial load of the webpage, which will be served on port 3000. We also specify that the /api/events endpoint will be used when any create, update, or delete operation takes place to keep the client and database in sync.

Set up the server side

Import the relevant libraries by adding the following lines to the top of the server.js file:

import express from 'express';
import bodyParser from 'body-parser';
import mysql from 'mysql2';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Now set up the Express app and database connection by adding the following code to server.js:

const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Static files
app.use(express.static(path.join(__dirname, "public")));
app.use(
  express.static(path.join(__dirname, "/node_modules/@bryntum/calendar"))
);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
// Database connection
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
});

Set up the server API endpoints

The client-side Bryntum library will make a POST request to the /api/events endpoint when changes are made to keep the data in the database in sync with the client-side UI.

Add the following HTTP POST request to the bottom of server.js:

// Add, update, and remove events
app.post('/api/events', async (req, res) => {
  const { added, updated, removed } = req.body.events;
  try {
    const addedResponse = added ? await addEvents(added) : [];
    const updatedResponse = updated ? await updateEvents(updated) : [];
    const removedResponse = removed ? await removeEvents(removed) : [];
    res.send({
      success: true,
      events: {
        rows: [...addedResponse, ...updatedResponse, ...removedResponse],
      },
    });
  } catch (error) {
    console.error(error);
    res.status(500).send({ success: false });
  }
});

This code defines an Express.js route handler for POST requests to the /api/events endpoint. It handles adding, updating, and removing events based on the request body. The addedupdated, and removed properties are destructured from req.body.events. A try-catch block is used to handle errors during database operations.

In the try block, addEventsupdateEvents, and removeEvents functions are conditionally called based on the presence of the respective properties. The results of these functions are then combined using the spread operator. If successful, the response sent to the client contains a success status and an events object with the combined results of the operations.

Define the addEvents function at the bottom of server.js:

// Helper functions
async function addEvents(events) {
  const query =
    'INSERT INTO events (name, startDate, endDate, duration, durationUnit, resourceId, allDay) VALUES (?, ?, ?, ?, ?, ?, ?)';
  const addedResponse = [];
  for (const event of events) {
    const {
      name,
      startDate,
      endDate,
      duration,
      durationUnit,
      resourceId,
      allDay,
    } = event;
    const [result] = await pool
      .promise()
      .query(query, [
        name,
        startDate,
        endDate,
        duration,
        durationUnit,
        resourceId,
        allDay,
      ]);
    addedResponse.push({
      id: result.insertId.toString(),
      $PhantomId: event.$PhantomId,
    });
  }
  return addedResponse;
}

The addEvents function inserts an array of events into the database. For each event, it extracts relevant properties, runs a SQL INSERT query, and stores the generated IDs in an array. The function returns this array after processing all events.

Define the updateEvents and removeEvents functions at the bottom of server.js:

async function updateEvents(events) {
  const updatedResponse = [];
  for (const event of events) {
    const id = await updateEvent(event);
    updatedResponse.push({ id });
  }
  return updatedResponse;
}
async function removeEvents(events) {
  const query = 'DELETE FROM events WHERE id = ?';
  for (const event of events) {
    await pool.promise().query(query, [event.id]);
  }
  return [];
}

The updateEvents function updates an array of events and returns an array of updated IDs. It iterates through the input events, calls the updateEvent function for each event, and pushes the returned ID into the updatedResponse array. Once all events are processed, the function returns the updatedResponse array.

The removeEvents function deletes events from the database. It takes an array of events and iterates through them, executing a SQL DELETE query for each event. It returns an empty array.

Next, define the updateEvent function:

async function updateEvent(event) {
  const { id, name, startDate, endDate, resourceId, allDay, recurrenceRule } =
    event;
  const updates = [];
  const values = [];
  // Add values and updates for each field
  if (name !== undefined) {
    updates.push('name = ?');
    values.push(name);
  }
  if (startDate !== undefined) {
    updates.push('startDate = ?');
    values.push(startDate);
  }
  if (endDate !== undefined) {
    updates.push('endDate = ?');
    values.push(endDate);
  }
  if (resourceId !== undefined) {
    updates.push('resourceId = ?');
    values.push(resourceId);
  }
  if (allDay !== undefined) {
    updates.push('allDay = ?');
    values.push(allDay);
  }
  if (recurrenceRule !== undefined) {
    updates.push('recurrenceRule = ?');
    values.push(recurrenceRule);
  }
  if (updates.length === 0) {
    throw new Error('No valid fields to update.');
  } else {
    const query = `UPDATE events SET ${updates.join(', ')} WHERE id = ?`;
    values.push(id);
    await pool.promise().query(query, values);
    return id;
  }
}

The updateEvent function updates a single event. It extracts relevant properties from the input event and creates an array of updates and values for each field. It then checks if any valid fields are available to update, throws an error if not, and otherwise executes a SQL UPDATE query using the pool.promise().query() method. It then returns the event ID.

Next, add the following HTTP GET request underneath the /api/events POST route:

// Get events and resources
app.get('/api/events', async (req, res) => {
  try {
    const [eventsResults] = await pool.promise().query('SELECT * FROM events');
    const [resourcesResults] = await pool
      .promise()
      .query('SELECT * FROM resources');
    const events = eventsResults.map((event) => ({
      ...event,
      id: event.id.toString(),
    }));
    const resources = resourcesResults.map((resource) => ({
      ...resource,
      id: resource.id.toString(),
    }));
    res.send({
      success: true,
      events: { rows: events },
      resources: { rows: resources },
    });
  } catch (error) {
    console.error(error);
    res.status(500).send({ success: false });
  }
});

This is the endpoint to fetch and read the data from our database. The library will call this endpoint to keep the client side up to date with our datastore, as we configured in the main.js script.

Create database tables compatible with Bryntum Calendar

If you followed the instructions in the README.md file, you will have already configured your database and populated it with some data.

The events table should look like this:

We will now alter the events table and also create a new table called resources to create a database compatible with the Bryntum Calendar implementation.

Run the following query to alter the events table for our events data:

ALTER TABLE events
ADD (
  duration DECIMAL(10, 2),
  durationUnit VARCHAR(10),
  cls VARCHAR(255),
  exceptionDates TEXT,
  resourceId INT,
  allDay TINYINT(1),
  recurrenceRule TEXT
);

Rename the startend, and title columns:

ALTER TABLE events
RENAME COLUMN start TO startDate,
RENAME COLUMN end TO endDate,
RENAME COLUMN title TO name;

Now update some columns in the table:

UPDATE events
SET
  duration = TIMESTAMPDIFF(HOUR, startDate, endDate),
  durationUnit = 'hour',
  cls = '',
  exceptionDates = NULL,
  resourceId = 1,
  allDay = 0,
  recurrenceRule = NULL
WHERE id;

This SQL code performs an update operation on the events table. For example, it sets the duration field to the difference in hours between the startDate time and endDate time fields for all records in the table.

Run the following query to create the resources table for our events data:

CREATE TABLE resources (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  eventColor VARCHAR(50)
);

The Bryntum Calendar needs resources, unlike the FullCalendar Calendar. Finally, add some data to the resources table:

INSERT INTO resources (name, eventColor)
VALUES ('Resource 1', 'green');

We have added this resource to demonstrate how events can be associated with specific resources using the resourceId field in the events table. The newly added resource has the name 'Resource 1' and a green eventColor.

Test the migration implementation

After completing the migration process, it’s time to test the implementation. Here are the steps to test it:

  1. Open the BryntumCalendar folder in the terminal.
  2. Run the following command:
npm run start

The Bryntum Calendar chart should display the migrated data from the FullCalendar chart. You can now take advantage of Bryntum Calendar’s extensive features and customization options to manage your project effectively.

Migrate from FullCalendar Scheduler to Bryntum Scheduler

FullCalendar Premium allows you to configure your Calendar to have timeline view. This view is useful if your calendar events have associated resources such as employees. The calendar has a resources column on the left. This timeline view is equivalent to the layout of the Bryntum Scheduler, so we’ll call it the FullCalendar Scheduler.

Create new database tables

Let’s create new database tables for the FullCalendar Scheduler. Run the following MySQL query in MySQL Workbench to create a table for the FullCalendar Scheduler events:

CREATE TABLE fullcalendar_scheduler_events (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `start` datetime NOT NULL,
  `end` datetime NOT NULL,
  `resourceId` int NOT NULL,
  PRIMARY KEY (`id`));

We’ve added an extra column called resourceId. This is the ID of the resource that the event is associated with.

Create a table for the resources data:

CREATE TABLE fullcalendar_scheduler_resources (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `eventBackgroundColor` varchar(255) NOT NULL,
  PRIMARY KEY (`id`));

Now add some example resource data to the resources table:

INSERT INTO fullcalendar_scheduler_resources (id, title, eventBackgroundColor)
VALUES (1, 'Peter', '#3183fe'),
       (2, 'Lisa', '#0076f8'),
       (3, 'Jennifer', '#9e25c3'),
       (4, 'Veronica', '#2055a5'),
       (5, 'James', '#1fba5e'),
       (6, 'Walter', '#fab007');

Add some example events data to the events table:

INSERT INTO fullcalendar_scheduler_events (id, title, start, end, resourceId)
VALUES (1, 'Meeting', '2023-12-04T08:00:00', '2023-12-04T11:00:00', 1),
       (2, 'Interview intern', '2023-12-04T13:00:00', '2023-12-04T14:00:00', 3),
       (3, 'Write up report', '2023-12-03T08:00:00', '2023-12-04T17:00:00', 4),
       (4, 'Presentation', '2023-12-04T14:00:00', '2023-12-04T16:00:00', 1),
       (5, 'Conference call', '2023-12-04T13:00:00', '2023-12-04T15:30:00', 6);

You’ll be able to view the example resources data by running the following query:

SELECT * FROM fullcalendar_scheduler_resources;

You’ll be able to view the example events data by running the following query:

SELECT * FROM fullcalendar_scheduler_events;

Set up the client side for the FullCalendar Scheduler

Move into the root directory of the project:

cd ..

In the public/index.html file, change the FullCalendar CDN link to use the premium fullcalendar-scheduler bundle:

<script src='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.8/index.global.min.js'></script>

We’ll now change and add some configuration properties of the FullCalendar Scheduler. Set the initial view to use the ‘resourceTimelineWeek’:

  initialView: 'resourceTimelineWeek',

Set the buttons at the end of the headerToolbar property to the following: end:

    end: 'addEventButton,deleteSelectedEventButton, resourceTimelineDay,resourceTimelineThreeDays,timeGridWeek,dayGridMonth,listWeek',

To give the resources column of the scheduler a header, add the following property below initialView: 'resourceTimelineWeek',:

  resourceAreaHeaderContent: "Workers",

Below the property you just added, add a resources property and set its URL: resources:

  resources: "http://localhost:3000/resources",

We’ll create another API endpoint for the resources.

In the eventReceiveeventDrop, and eventResize methods, add the following resourceId property to the eventData object:

  resourceId: info.event._def.resourceIds[0],

In the createEvent function, add the following resourceId property to the newEvent object:

  resourceId: eventData.resourceId,

Replace the showAddEventForm function with the following lines of code that add a prompt to enter the resource ID:

      function showAddEventForm() {
        const title = prompt('Enter the event title:');
        if (title) {
          const resourceId = prompt('Enter the resource ID');
          if (resourceId) {
            const start = prompt(
              'Enter the start date and time in YYYY-MM-DDTHH:mm format:'
            );
            if (start) {
              const end = prompt(
                'Enter the end date and time in YYYY-MM-DDTHH:mm format:'
              );
              if (end) {
                createEvent({ title, start, end, resourceId });
                } else {
                  alert('Invalid end date and time.');
                }
            } else {
              alert('Invalid start date and time.');
            }
          } else {
            alert('Invalid resource ID.');
          }
        } else {
          alert('Invalid title.');
        }
      }

We’ll now adjust the backend APIs.

Set up the server side for the FullCalendar Scheduler

In the server.js file in the root folder, change the table that the events are queried from in the GET /events API route:

"SELECT * FROM fullcalendar_scheduler_events"

Add a new route to get the resources:

app.get("/resources", (req, res) => {
  db.query("SELECT * FROM fullcalendar_scheduler_resources", (err, results) => {
    if (err) throw err;
    res.send(results);
  });
});

In the POST /events API route, get the event fields including the resourceId from the request body:

  const { title, start, end, resourceId } = req.body;

Change the database query to insert the values into the correct table and add the resourceId value:

db.query(
    "INSERT INTO fullcalendar_scheduler_events (title, start, end, resourceId) VALUES (?, ?, ?, ?)",
    [title, start, end, resourceId],

In the PUT /events/:id API route for updating events, get the event fields including the resourceId from the request body:

  const { start, end, resourceId } = req.body;

Change the database query to update the values in the correct table and add the resourceId value:

db.query(
    "UPDATE fullcalendar_scheduler_events SET start = ?, end = ?, resourceId = ? WHERE id = ?",
    [start, end, resourceId, id],

In the DELETE /events/:id API route, change the table that events are deleted from:

DELETE FROM fullcalendar_scheduler_events

You’ll now have a FullCalendar Scheduler with CRUD functionality.

Let’s migrate this FullCalendar Scheduler to Bryntum Scheduler.

Install and set up Bryntum Scheduler

Move into the Bryntum Calendar directory:

cd BryntumCalendar

Install Bryntum Scheduler by following step 1 and step 4 of the vanilla JavaScript with npm set-up guide.

Set up the client side

Replace the code in the BryntumCalendar/public/main.js file with the following:

import { Scheduler } from "./scheduler.module.js";
const scheduler = new Scheduler({
  appendTo: document.body,
  date: new Date(2023, 11, 4, 8),
  viewPreset: "hourAndDay",
  crudManager: {
    autoLoad: true,
    autoSync: true,
    transport: {
      load: {
        url: "http://localhost:3000/api/events",
      },
      sync: {
        url: "http://localhost:3000/api/events",
      },
    },
  },
  columns: [
    {
      type: "resourceInfo",
      text: "Workers",
      width: 160,
      editor: false,
    },
  ],
});

The config for the Bryntum Scheduler is similar to the config for the Bryntum Calendar. There is an added columns property for the resources that will be displayed on the left-hand side of the Scheduler.

Set up the server API endpoints

Update the BryntumCalendar/server.js file to serve the Bryntum Scheduler library code at the base URL for convenient importing of the library:

app.use(
  express.static(path.join(__dirname, "/node_modules/@bryntum/scheduler"))
);

Now update the database queries in the API route handler functions to query the new database tables. Change events to fullcalendar_scheduler_events and resources to fullcalendar_scheduler_resources.

Create database tables compatible with Bryntum Scheduler

We’ll now alter the fullcalendar_scheduler_events table to make it compatible with the Bryntum Scheduler implementation.

Run the following query to alter the fullcalendar_scheduler_events table for our Bryntum Scheduler events data:

ALTER TABLE fullcalendar_scheduler_events
ADD (
  duration DECIMAL(10, 2),
  durationUnit VARCHAR(10),
  cls VARCHAR(255),
  exceptionDates TEXT,
  allDay TINYINT(1),
  recurrenceRule TEXT
);

Rename the startend, and title columns:

ALTER TABLE fullcalendar_scheduler_events
RENAME COLUMN start TO startDate,
RENAME COLUMN end TO endDate,
RENAME COLUMN title TO name;

Update some columns in the table:

UPDATE fullcalendar_scheduler_events
SET
  duration = TIMESTAMPDIFF(HOUR, startDate, endDate),
  durationUnit = 'hour',
  cls = '',
  exceptionDates = NULL,
  allDay = 0,
  recurrenceRule = NULL
WHERE id;

We’ll now alter the fullcalendar_scheduler_resources table to make it compatible with the Bryntum Scheduler implementation.

Run the following query to update the fullcalendar_scheduler_resources table:

ALTER TABLE fullcalendar_scheduler_resources
RENAME COLUMN title TO name,
RENAME COLUMN eventBackgroundColor TO eventColor;

Test the migration implementation

Start the development server:

npm run start

The Bryntum Scheduler should display the migrated data from the FullCalendar Scheduler.

You can add headshots of the resources BryntumCalendar/public with (e.g., peter.png) to show images instead of the name initial.

You have now successfully migrated your FullCalendar Scheduler to Bryntum Scheduler. Note that you can double-click on events to edit them, unlike the FullCalendar Scheduler. You can change the timeline range by right-clicking on one of the date or time headings at the top of the scheduler and adjusting the zoom. You can also hold the Windows Ctrl key or macOS ⌘ key and use your mouse wheel to change the zoom when your cursor is over the timeline.

Differences between FullCalendar and Bryntum

These migrations demonstrate how the FullCalendar and Bryntum components are implemented differently. Let’s look at some of the major differences.

Data loading and saving

FullCalendar receives events and resources data from the backend using the events property and resources property. To save data changes, we needed event listeners and custom functions that use the fetch API to perform CRUD updates to the backend.

The Bryntum Crud Manager simplifies loading data and saving changes in the Bryntum components.

Columns and custom rendering

Columns of the FullCalendar Scheduler are defined by the resourceAreaColumns property, which has several configuration options, like width and grouping, and can take custom content. You can add custom content to FullCalendar in various places using the eventContent property, allowing for flexibility in creating the UI.

The Bryntum Scheduler columns config property has many configuration options including width, grouping, and custom content rendering. The Bryntum components also allow for custom rendering using methods like cellRendererheaderRenderer, and tooltipRenderer.

Resource cell editing

To make the resource text editable in the FullCalendar Scheduler, you need to use a resourceLabelContent function to custom render the resource cells. For example, you could render the resource label as an input so that it can be edited.

The Bryntum Scheduler resource cells can be editable. All you need to do is set the `editor` property in the `columns` config property to `true`, which is the default value. You’ll also need to update the API endpoints to persist the resource edits.

Styling

FullCalendar has Bootstrap themes available and you can customize the CSS.

Bryntum Scheduler can be styled using five available themes and you can create custom themes. Read more about styling Bryntum Scheduler here.

Conclusion

Both FullCalendar and Bryntum are full-featured components but Bryntum Calendar and Bryntum Scheduler have more features out of the box that are more advanced and flexible, making these components better suited to complex schedulers and calendars. Take a look at the following demo pages to get an idea of what each component is capable of:

Arsalan Khattak

Bryntum Calendar Bryntum Scheduler