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 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:
- Installing and configuring Bryntum Calendar.
- Establishing API endpoints for the server.
- Creating database tables that are compatible with Bryntum Calendar.
- 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 added
, updated
, 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, addEvents
, updateEvents
, 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 start
, end
, 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:
- Open the
BryntumCalendar
folder in the terminal. - 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 eventReceive
, eventDrop
, 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 start
, end
, 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 cellRenderer
, headerRenderer
, 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: