DEV Community

Bryntum for Bryntum

Posted on

Real-Time Project Management with Bryntum Gantt

A real-time application, as the name implies, is an application that is updated in or close to real time. A good example of a real-time app is Google Docs. When it was launched in 2006, Google Docs changed the way multiple people collaborated on documents, enabling users to receive updates as they're made.

Industries such as gaming, e-commerce, and social media employ real-time web apps to facilitate seamless collaboration and improve reactivity to remote changes.

In this article, you'll learn how to build a real-time project management app using React, Socket.IO, and Bryntum Gantt, a powerful tool that can help you easily set up project tasks, track their progress, and collaborate with team members.

What Are Real-Time Web Apps?

Real-time web apps are web applications that allow for information to be passed between the server and the client in real time. They are constantly changing as soon as new data is received.

Real-time web apps use the WebSocket protocol, which provisions full-duplex (or two-way) communication between a client and a server. As opposed to a WebSocket, the HTTP used in traditional web apps has a unidirectional (or half-duplex) communication. The following image shows how a half-duplex communication protocol like HTTP and a full-duplex communication protocol like WebSocket transfer data between a server and a client:

Half-duplex vs. full-duplex courtesy of Gideon Idoko

WebSockets make it possible for the client and server to send and receive data in real time over a single transmission control protocol (TCP) socket connection. A WebSocket connection is persistent and is kept open until either the server or the client terminates it. In contrast, an HTTP connection is temporary, as it's closed as soon as the response from the server is received. If you need to make a new request, a fresh connection has to be created.

Real-time web apps have several advantages over traditional web apps. They are more responsive, reliable, scalable, and able to handle more concurrent users who need to receive updates in real time.

Building a Real-Time Project Management App

Now that you know what real-time web apps and WebSockets are, it's time to learn how to leverage them with React, Socket.IO, and Bryntum Gantt to create a simple but real-time project task management tool. The application will work like this at the end of this article:

Click to view gif

You can find the full code for this tutorial on this GitHub repo.

Prerequisites

Before you begin, you'll need to have Node.js installed, as it's needed to run React in development mode on your local machine. Node.js also ships with the Node Package Manager (npm), which enables the installation of some required packages, like Bryntum Gantt. If you haven't already, download the LTS version of Node.js and install it.

Bryntum Gantt is the most reliable, easy-to-use, and full-featured JavaScript Gantt chart component. It's compatible with all major browsers and offers a wide range of features that can be fully customized to suit your specific needs. Bryntum Gantt's rendering engine is fast, and it can handle large data sets without halting performance. In addition, its built-in scheduling engine provides asynchronous rescheduling of any number of tasks.

Bryntum Gantt will provide the Gantt chart and other necessary components, like buttons, that will be configured to meet the needs of the application. Now it's time to build it.

Basic Setup

The app will be bootstrapped with Create React App (CRA). Thankfully, Bryntum has a CRA template. This will enable users to quickly get started with the Bryntum Gantt package. Bryntum packages are commercial, but they have trial versions that will be sufficient for this tutorial.

The commercial and trial versions are available in Bryntum's private registry. Open your terminal and run the following command to locate the registry:

npm config set "@bryntum:registry=https://npm.bryntum.com"
Enter fullscreen mode Exit fullscreen mode

Then log in:

npm login --registry=https://npm.bryntum.com
Enter fullscreen mode Exit fullscreen mode

You will be required to enter your username, password, and email address. Use your email with the "@" replaced with ".." as your username and "trial" as your password:

Username: user..yourdomain.com
Password: trial
Email: (this IS public) user@yourdomain.com
Enter fullscreen mode Exit fullscreen mode

This will give you access to Bryntum's private registry and packages.

Now that you have access to the registry, it's time to finish up your setup. To do so, create a new directory for the project:

mkdir realtime-proman
Enter fullscreen mode Exit fullscreen mode

You can replace realtime-proman with your preferred app name.

Then bootstrap a new React client with the Bryntum Gantt template:

npx create-react-app client --template @bryntum/cra-template-javascript-gantt
Enter fullscreen mode Exit fullscreen mode

Since a WebSocket will be used, a simple server is required. Create a new directory for the server-side code in the root directory of your project:

mkdir server
Enter fullscreen mode Exit fullscreen mode

Change to the server directory and initialize a new npm project:

cd server && npm init -y
Enter fullscreen mode Exit fullscreen mode

Finally, create an index.js file that will contain the server-side code:

touch index.js
Enter fullscreen mode Exit fullscreen mode

Creating a WebSocket Server

Now you need to create a simple WebSocket server that your React client can interact with. In the server directory, run the following command to install the server-required dependency, Socket.IO:

npm install socket.io
Enter fullscreen mode Exit fullscreen mode

Then in the index.js file you created earlier, paste the following code:

const 
    { Server } = require('socket.io'),
    io = new Server({
        cors: { origin: '*' }
    });
// data will be persisted here
let persistedData = null;
io.on('connection', (socket) => {
    console.log(`New connection ID: ${socket.id}`);
    socket.emit('just-joined', persistedData ?? {});
    socket.on('data-change', (data) => {
        persistedData = data;
        socket.broadcast.emit('new-data-change', data);
    })
});
io.listen(5000);
console.log(`๐Ÿš€ socket server started on port 5000...`);
Enter fullscreen mode Exit fullscreen mode

Here, you have three events: data-change, new-data-change, and just-joined. When the data-change event is fired, the passed data is persisted to persistData, and the new-data-change event is emitted with that data. The just-joined event is emitted with the persisted data on every new connection.

Finally, start the WebSocket server:

node index
Enter fullscreen mode Exit fullscreen mode

Working on the Client

Now that the WebSocket server is up and running, it's time to finish up on the client side. A peek in the client directory will reveal its structure, which looks like this:

client                                                          
โ”œโ”€ public                                                                        
โ”‚  โ”œโ”€ data                                                                       
โ”‚  โ”‚  โ””โ”€ gantt-data.json                                                         
โ”‚  โ”œโ”€ favicon.png                                                                
โ”‚  โ”œโ”€ index.html                                                                 
โ”‚  โ””โ”€ manifest.json                                                              
โ”œโ”€ src                                                                           
โ”‚  โ”œโ”€ App.js                                                                     
โ”‚  โ”œโ”€ App.scss                                                                   
โ”‚  โ”œโ”€ GanttConfig.js                                                             
โ”‚  โ””โ”€ index.js                                                                   
โ”œโ”€ package-lock.json                                                             
โ”œโ”€ package.json                                                                  
โ””โ”€ README.md
Enter fullscreen mode Exit fullscreen mode

The gantt-data.json file holds some dummy data that is loaded once the App component renders. Then the GanttConfig.js file holds the configuration for your Gantt chart.

Create a new terminal window in the client directory and spin up your React development server:

npm start
Enter fullscreen mode Exit fullscreen mode

When the React development server is up, you should see something like the following:

Initial app

The data you see here is from the gantt-data.json file and is bound to the BryntumGantt via the project transport method when the app first loads. This is defined in the Gantt chart configuration in the GanttConfig.js file like so:

// ...
project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    }
// ...
Enter fullscreen mode Exit fullscreen mode

The path to the gantt-data.json file is passed as the value for the url option. A GET request is made to the path to fetch its JSON data when the App component initially renders. This is why the content of the gantt-data.json file resembles a server response. It even has a success key with true value. Aside from the success key, you'll find seven root keys:

  1. project: holds the configuration for your ProjectModel, which is a central store for all the data in your project.

Please note: A model is simply the definition of a record that can be added to (or loaded into) a store. A store is a data container that holds flat data or tree structures. A record is a data container and an item in a store.

  1. calendars: holds all calendars passed to the CalendarModel.

  2. tasks: holds all the tasks passed to the TaskModel.

  3. dependencies: holds all the single dependencies between tasks. It's passed to the DependencyModel.

  4. resources: holds all resources or assets passed to the ResourceModel.

  5. assignments: holds all single assignments of a resource to a task passed to the AssignmentModel.

  6. timeRanges: holds all ranges of time in the timeRangeStore of the ProjectModel. Each range represents a TimeSpan model.

Now, it's time to make use of the default JSON data loaded into the Gantt chart.

Adding a Toolbar

To begin, you need to add a toolbar to the Gantt chart that will have two button components for creating and editing tasks, respectively.

Create a GanttToolbar.js file in the src folder of your React client and add the following code to it:

import {
    Toolbar,
    Toast,
} from '@bryntum/gantt';

export default class GanttToolbar extends Toolbar {
    // Factoryable type name
    static get type() {
        return 'gantttoolbar';
    }

    static get $name() {
        return 'GanttToolbar';
    }

    // Called when toolbar is added to the Gantt panel
    set parent(parent) {
        super.parent = parent;

        const me = this;

        me.gantt = parent;

        me.styleNode = document.createElement('style');
        document.head.appendChild(me.styleNode);
    }

    get parent() {
        return super.parent;
    }

    static get configurable() {
        return {
            items: [
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            color    : 'b-green',
                            ref      : 'addTaskButton',
                            icon     : 'b-fa b-fa-plus',
                            text     : 'Create',
                            tooltip  : 'Create new task',
                            onAction : 'up.onAddTaskClick'
                        }
                    ]
                },
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            ref      : 'editTaskButton',
                            icon     : 'b-fa b-fa-pen',
                            text     : 'Edit',
                            tooltip  : 'Edit selected task',
                            onAction : 'up.onEditTaskClick'
                        }
                    ]
                },
            ]
        };
    }

    async onAddTaskClick() {
        const { gantt } = this,
              added     = gantt.taskStore.rootNode.appendChild({
                name     : 'New task',
                duration : 1
            });

        // wait for immediate commit to calculate new task fields
        await gantt.project.commitAsync();

        // scroll to the added task
        await gantt.scrollRowIntoView(added);

        gantt.features.cellEdit.startEditing({
            record : added,
            field  : 'name'
        });
    }

    onEditTaskClick() {
        const { gantt } = this;

        if (gantt.selectedRecord) {
            gantt.editTask(gantt.selectedRecord);
        } else {
            Toast.show('First select the task you want to edit');
        }
    }
}

// Register this widget type with its Factory
GanttToolbar.initClass();
Enter fullscreen mode Exit fullscreen mode

This registers a container widget with two buttons in it.

Update your GanttConfig.js with the following:

import './GanttToolbar';
/**
 * Application configuration
 */

const ganttConfig = {
    columns    : [
        { type : 'name', field : 'name', width : 250 }
    ],
    viewPreset : 'weekAndDayLetter',
    barMargin  : 10,

    project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    },
    tbar : { type: 'gantttoolbar' }, // toolbar
};

export { ganttConfig };
Enter fullscreen mode Exit fullscreen mode

This imports the GanttToolbar.js file and adds the registered toolbar type to the tbar option of the Gantt chart configuration.

Save all the files and reload your browser to view the new toolbar:

App with toolbar

Also, you should be able to create a new task or edit existing ones by clicking on the CREATE or EDIT buttons, respectively.

At this point, you've created a project task management tool, but it's not in real time yet. Your React client has to exchange data with the WebSocket server for it to be real time.

Implementing WebSocket on the Client

To implement WebSocket on the client, start by installing the Socket.IO client library, along with a universally unique identifier (UUID) package that will help to identify clients in a socket connection.

Open a terminal in the client directory and run the following command:

npm install socket.io-client uuid
Enter fullscreen mode Exit fullscreen mode

Once the installation is complete, import the io and v4 methods from the installed packages and the useEffect and useRef hooks from react in the App.js file:

import { useRef, useEffect } from 'react';
import { io } from 'socket.io-client';
import { v4 as uuid } from 'uuid';
Enter fullscreen mode Exit fullscreen mode

Initialize the socket client just before the App component definition:

const socket = io('ws://localhost:5000', { transports: ['websocket'] });
Enter fullscreen mode Exit fullscreen mode

Update the App component with the following code:

function App() {
    const 
        ganttRef = useRef(null), // reference to the gantt chart
        remote = useRef(false), // set to true when changes are made to the store
        clientId = useRef(uuid()), // a unique ID for the client
        persistedData = useRef(), // store persisted data from the server
        canSendData = useRef(false); // set to true to allow data to be sent to the server

    useEffect(() => {
        const gantt = ganttRef.current?.instance;
        if (gantt) {
            // access the ProjectModel
            const project = gantt.project;
            // listen for change events
            project?.addListener('change', () => {
                // don't send data when the same client is updating the project store
                if (!remote.current) {
                    // only send data when allowed
                    if (canSendData.current) {
                        // get project store
                        const store = gantt.store.toJSON();
                        // send data to the store with the data-change event
                        socket.emit('data-change', { senderId: clientId.current, store });
                    }
                }
            });
            // trigger when project data is loaded
            project?.addListener('load', () => {
                if (persistedData.current) {
                    const { store } = persistedData.current;
                    remote.current = true;
                    if (store) {
                        // update project store if persisted store exists
                        gantt.store.data = store
                    }
                    remote.current = false;
                    // allow data to be sent 2s after data is loaded
                    setTimeout(() => canSendData.current = true, 2000);
                    // stop listening since this data from this event is needed once
                    socket.off('just-joined');
                }
            });
            socket.on('just-joined', (data) => {
                // update with the persisted data from the server
                if (data) persistedData.current = data;
            });

            socket.on('new-data-change', ({ senderId, store }) => {
                // should not update store if received data was sent by same client
                if (clientId.current !== senderId) {
                    // disable sending sending data to server as change to the store is to be made
                    remote.current = true;
                    // don't update store if previous data was sent by same client
                    if (JSON.stringify(gantt.store.toJSON()) !== JSON.stringify(store)) {
                        // update store with store from server
                        gantt.store.data = store;
                    }
                    // data can now be sent to the server again
                    remote.current = false;
                }
            });
        }
        return () => {
            // disconnect socket when this component unmounts
            socket.disconnect();
        };
    }, []);

    return (
        <BryntumGantt
            ref = {ganttRef}
            {...ganttConfig}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

The comments on the code explain what each line does, but the state of the project store is sent and received via the WebSocket. The project store is updated upon receiving a new state from the WebSocket server.

Save and reload your app in the browser. Then open your app in another browser window and create or edit tasks in the app in one of the windows. You should see the changes reflected in real time in the app in the other window as follows:

Click to view gif

Conclusion

Real-time web apps offer an engaging user experience that lets multiple individuals interact with the app in real time. In this article, you were introduced to what real-time web apps are and what makes them real time. You also learned how to use the Bryntum Gantt chart, React, and a WebSocket to build a simple real-time project task management app. In the process, you learned how to create a WebSocket server and how to communicate with it from a React client.

Bryntum is an industry leader when it comes to world-class web components. They provide powerful and mature components, like Bryntum Gantt, which was used here. Other web components from Bryntum include the Bryntum Scheduler, Bryntum Grid, Bryntum Calendar, Bryntum Task Board, and even testing tools like Siesta. These components are highly customizable, easily integrated, and you can try them for free today!

Are you already using a Bryntum web component? Let us know what you think!

Top comments (0)