Implementing Persistent UI State in DHTMLX Gantt

In modern web applications, maintaining the state of UI elements between page reloads is crucial for a smooth user experience.

In this tutorial, we’ll guide you through the simple implementation of the persistent UI in DHTMLX Gantt. We’ll focus on a small subset of features – namely expanded or collapsed branches of tasks, and the selected zoom level of the Gantt. You’ll learn how these settings can be stored in the local storage of the browser and restored later, so the Gantt remains consistent even after the page is reloaded. By following this guide, you can enable end-users of your app to continue where they left off without the need to reconfigure their settings, thus saving time and enhancing productivity.

Why You Need Persistent UI State in Gantt

Before we dive into code, let’s consider the goal of this tutorial in more detail.

In the example below, you can collapse the whole project into a compact view, expand it back, and switch between zoom levels of Gantt using a simple toolbar. However, all the changes will be lost once the page is reloaded. It is quite natural since dynamic UI changes are rarely saved to the database. But, in practice, it can be very inconvenient for end-users.

Check the sample >

For comparison, you can also test the second sample below, where the persistent state is implemented.

Check the sample >

Try switching the Zoom level or expanding or collapsing some branches in the Gantt chart and then reload the page. You will see the Gantt chart with all the changes introduced before the reloading. This is one of those quality-of-life improvements that can contribute to a better user experience with Gantt.

Now it is time to show you how to integrate the persistent UI state into your JavaScript Gantt chart step-by-step.

Step 1: Setting Up The Gantt

For this tutorial, it will be enough to create a simple Gantt chart and add a toolbar to it for quick actions such as zooming and expanding branches.

Let’s set it up.

You need to create the HTML layout.

<!DOCTYPE html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Persistent UI State in DHTMLX Gantt</title>
    <script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
    <link rel="stylesheet" href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css">
    <style>
        html,
        body {
            height: 100%;
            padding: 0px;
            margin: 0px;
            overflow: hidden;
        }


        body {
            display: flex;
            flex-direction: column;
        }


        #toolbar {
            display: flex;
            padding: 12px;
            gap: 12px;
            justify-content: center;
            flex-shrink: 0;
        }


        #gantt_here {
            flex-basis: 100%;
        }
    </style>
</head>

<body>
    <div id="toolbar">
        <button id="expandAll">Expand All</button>
        <button id="collapseAll">Collapse All</button>
        <label>
            Zoom: <input type="range" id="zoomSlider" min="0" max="5" step="1" value="1">
        </label>
    </div>
    <div id="gantt_here" style='width:100%; height:100%;'></div>
    <script>
    </script>

</body>

Here you create a container for the Gantt chart and place a Toolbar on top of it. The toolbar contains buttons for expanding and collapsing all tasks and a slider for adjusting the zoom level.
These controls will be enabled right after the Gantt initialization described in the next step.

Configure and Initialize the Gantt Chart

At this stage, you create a basic Gantt configuration using the Zoom extension. Don’t be intimidated by the amount of code, most of this is boilerplate timescale configuration and test data.

gantt.config.open_tree_initially = true;
gantt.ext.zoom.init({
    levels: [
        {
            name: "hour",
            scale_height: 50,
            min_column_width: 30,
            scales: [
                { unit: "day", step: 1, format: "%d %M" },
                { unit: "hour", step: 1, format: "%H" }
            ]
        },
        {
            name: "day",
            scale_height: 50,
            min_column_width: 80,
            scales: [
                { unit: "month", format: "%F, %Y" },
                { unit: "day", step: 1, format: "%d %M" }
            ]
        },
        {
            name: "week",
            scale_height: 50,
            min_column_width: 50,
            scales: [
                { unit: "month", format: "%F, %Y" },
                {
                    unit: "week", step: 1, format: function (date) {
                        const dateToStr = gantt.date.date_to_str("%d %M");
                        const endDate = gantt.date.add(date, 6, "day");
                        const weekNum = gantt.date.date_to_str("%W")(date);
                        return "#" + weekNum + ", " + dateToStr(date) + " - " + dateToStr(endDate);
                    }
                },
                { unit: "day", step: 1, format: "%j %D" }
            ]
        },
        {
            name: "month",
            scale_height: 50,
            min_column_width: 120,
            scales: [
                { unit: "month", format: "%F, %Y" },
                { unit: "week", format: "Week #%W" }
            ]
        },
        {
            name: "quarter",
            height: 50,
            min_column_width: 90,
            scales: [
                { unit: "month", step: 1, format: "%M" },
                {
                    unit: "quarter", step: 1, format: function (date) {
                        const dateToStr = gantt.date.date_to_str("%M");
                        const endDate = gantt.date.add(gantt.date.add(date, 3, "month"), -1, "day");
                        return dateToStr(date) + " - " + dateToStr(endDate);
                    }
                }
            ]
        },
        {
            name: "year",
            scale_height: 50,
            min_column_width: 30,
            scales: [
                { unit: "year", step: 1, format: "%Y" }
            ]
        }
    ]
});
gantt.ext.zoom.setLevel(1);
gantt.init("gantt_here");
gantt.parse({
    data: [
        { id: 1, text: "Office itinerancy", type: "project", start_date: "02-04-2024 00:00", duration: 17, progress: 0.4, parent: 0 },
        { id: 2, text: "Office facing", type: "project", start_date: "02-04-2024 00:00", duration: 8, progress: 0.6, parent: "1" },
        { id: 3, text: "Furniture installation", type: "project", start_date: "11-04-2024 00:00", duration: 8, parent: "1", progress: 0.6, },
        { id: 4, text: "The employee relocation", type: "project", start_date: "13-04-2024 00:00", duration: 5, parent: "1", progress: 0.5 },
        { id: 5, text: "Interior office", type: "task", start_date: "03-04-2024 00:00", duration: 7, parent: "2", progress: 0.6 },
        { id: 6, text: "Air conditioners check", type: "task", start_date: "03-04-2024 00:00", duration: 7, parent: "2", progress: 0.6 },
        { id: 7, text: "Workplaces preparation", type: "task", start_date: "12-04-2024 00:00", duration: 8, parent: "3", progress: 0.6, },
        { id: 8, text: "Preparing workplaces", type: "task", start_date: "14-04-2024 00:00", duration: 5, parent: "4", progress: 0.5 },
        { id: 9, text: "Workplaces importation", type: "task", start_date: "21-04-2024 00:00", duration: 4, parent: "4", progress: 0.5 },
        { id: 10, text: "Workplaces exportation", type: "task", start_date: "27-04-2024 00:00", duration: 3, parent: "4", progress: 0.5 },
        { id: 11, text: "Product launch", type: "project", progress: 0.6, start_date: "02-04-2024 00:00", duration: 13, parent: 0 },
        { id: 12, text: "Perform Initial testing", type: "task", start_date: "03-04-2024 00:00", duration: 5, parent: "11", progress: 1 },
        { id: 13, text: "Development", type: "project", start_date: "03-04-2024 00:00", duration: 11, parent: "11", progress: 0.5 },
        { id: 14, text: "Analysis", type: "task", start_date: "03-04-2024 00:00", duration: 6, parent: "11", owner: [], progress: 0.8 },
        { id: 15, text: "Design", type: "project", start_date: "03-04-2024 00:00", duration: 5, parent: "11", progress: 0.2 },
        { id: 16, text: "Documentation creation", type: "task", start_date: "03-04-2024 00:00", duration: 7, parent: "11", progress: 0 },
        { id: 17, text: "Develop System", type: "task", start_date: "03-04-2024 00:00", duration: 2, parent: "13", progress: 1 },
        { id: 25, text: "Beta Release", type: "milestone", start_date: "06-04-2024 00:00", parent: "13", progress: 0, duration: 0 },
        { id: 18, text: "Integrate System", type: "task", start_date: "10-04-2024 00:00", duration: 2, parent: "13", progress: 0.8 },
        { id: 19, text: "Test", type: "task", start_date: "13-04-2024 00:00", duration: 4, parent: "13", progress: 0.2 },
        { id: 20, text: "Marketing", type: "task", start_date: "13-04-2024 00:00", duration: 4, parent: "13", progress: 0 },
        { id: 21, text: "Design database", type: "task", start_date: "03-04-2024 00:00", duration: 4, parent: "15", progress: 0.5 },
        { id: 22, text: "Software design", type: "task", start_date: "03-04-2024 00:00", duration: 4, parent: "15", progress: 0.1 },
        { id: 23, text: "Interface setup", type: "task", start_date: "03-04-2024 00:00", duration: 5, parent: "15", progress: 0 },
        { id: 24, text: "Release v1.0", type: "milestone", start_date: "20-04-2024 00:00", parent: "11", progress: 0, duration: 0 }
    ],
    links: [
        { id: "2", source: "2", target: "3", type: "0" },
        { id: "3", source: "3", target: "4", type: "0" },
        { id: "7", source: "8", target: "9", type: "0" },
        { id: "8", source: "9", target: "10", type: "0" },
        { id: "16", source: "17", target: "25", type: "0" },
        { id: "17", source: "18", target: "19", type: "0" },
        { id: "18", source: "19", target: "20", type: "0" },
        { id: "22", source: "13", target: "24", type: "0" },
        { id: "23", source: "25", target: "18", type: "0" }
    ]
});

The next step is to activate the toolbar.

First, you add buttons that will help expand and collapse all tasks:

// Toolbar handlers
document.getElementById('expandAll').addEventListener('click', function () {
    gantt.eachTask(function (task) {
        task.$open = true;
    });
    gantt.render();
});


document.getElementById('collapseAll').addEventListener('click', function () {
    gantt.eachTask(function (task) {
        task.$open = false;
    });
    gantt.render();
});

After that, you add the zoom slider functionality for zooming the Gantt chart in and out.

document.getElementById('zoomSlider').addEventListener('input', function () {
    const zoomLevel = parseInt(this.value, 10);
    gantt.ext.zoom.setLevel(zoomLevel);
});
Step 2. Save and Restore the State of Expanded and Collapsed Tasks

Now we can finally get to the main objective of this tutorial, namely saving the state of branches and restoring it after page reload.

You can store the state using localStorage as follows:

// State of opened/closed branches
function saveBranchesState() {
    const state = {};
    gantt.eachTask((task) => {
        state[task.id] = !!task.$open;
    });
    localStorage.setItem('gantt_expanded_tasks', JSON.stringify(state));
}
function restoreBranchesState() {
    const expandedTasks = JSON.parse(localStorage.getItem('gantt_expanded_tasks') || "{}");
    gantt.eachTask((task) => {
        if (expandedTasks[task.id] !== undefined) {
            task.$open = expandedTasks[task.id];
        }
    })


    gantt.render();
}

You need to save the state each time a branch is expanded or collapsed, and restore it when the tasks are loaded into the Gantt:

gantt.attachEvent("onTaskClosed", saveBranchesState);
gantt.attachEvent("onTaskOpened", saveBranchesState);
gantt.attachEvent("onParse", restoreBranchesState, { once: true });

It is also necessary to save the Gantt state when a user presses Expand All / Collapse All buttons in the toolbar:

// Toolbar handlers
document.getElementById('expandAll').addEventListener('click', function () {
    …
    saveBranchesState();
});

document.getElementById('collapseAll').addEventListener('click', function () {
    …
    saveBranchesState();
});
Step 3. Save and Restore State of Zoom

Similarly, you implement functions that would store and restore the state of Zoom.

function saveZoomState() {
    const zoomLevel = gantt.ext.zoom.getCurrentLevel();
    localStorage.setItem('gantt_zoom_level', zoomLevel);
    document.getElementById('zoomSlider').value = zoomLevel;
}


function restoreZoomState() {
    const zoomLevel = localStorage.getItem('gantt_zoom_level');
    if (zoomLevel) {
        gantt.ext.zoom.setLevel(zoomLevel);
        document.getElementById('zoomSlider').value = zoomLevel;
    }
}

The state should be saved when end-users change the zoom level in the Toolbar and restored when the Gantt is initialized on the page:

document.getElementById('zoomSlider').addEventListener('input', function () {
    const zoomLevel = parseInt(this.value, 10);
    gantt.ext.zoom.setLevel(zoomLevel);
    saveZoomState();
});
gantt.attachEvent("onGanttReady", restoreZoomState);

Following the steps above, you can implement the permanent Gantt state like in this sample.

Conclusion

This tutorial clearly explains how to build a Gantt chart that preserves the state of expanded/collapsed branches and the zoom level between page reloads. This implementation enhances the user experience by ensuring their customizations are maintained, thus saving time and effort.

Depending on the configuration of your Gantt chart and the settings that you use, you may need to store more changes than we demonstrated here. We’ll cover more complex scenarios in our future tutorials.

Related Materials

Advance your web development with DHTMLX

Gantt chart
Event calendar
Diagram library
30+ other JS components