Skip to main content

DHTMLX. Kanban, Gantt, Event Calendar, To do list with real-time backend sync example

This demo showcases a full project management application built with four DHTMLX widgets: Kanban, Gantt, Event Calendar, and To Do List, all connected to a shared backend with real-time multiuser synchronization. A tabbed layout lets users switch between views while all widgets stay in sync through WebSocket connections.

Live example

let activeView = "kanban";
// Check Go backend repository here - https://github.com/DHTMLX/project-management-go
// const serverURL = "http://localhost:3000";
const serverURL = "https://docs.dhtmlx.com/demos/project-management";
login(serverURL + "/login?id=1")
    .then((token) => {
        initKanban(token, serverURL);
        initTodo(token, serverURL);
        initGantt(token, serverURL);
        initCalendar(token, serverURL);
    })
    .catch((err) => {
        console.error(err);
    });

// Layout initialization
const layout = new dhx.Layout("layout", {
    rows: [
        {
            id: "toolbar",
            height: "content"
        },
        {
            id: "tabbar",
            height: "content"
        },
        {
            html: `
                <main id="views">
                    <section id="kanban">
                        <div id="kanban_toolbar"></div>
                        <div id="kanban_root"></div>
                    </section>
                    <section id="todo">
                        <div id="todo_toolbar"></div>
                        <div id="todo_root"></div>
                    </section>
                    <section id="gantt">
                        <div id="gantt_root"></div>
                    </section>
                    <section id="calendar">
                        <div id="calendar_root"></div>
                    </section>
                </main>
            `
        }
    ]
});

// Toolbar initialization
const toolbar = new dhx.Toolbar(null);
// loading data into Toolbar
toolbar.data.parse(toolbarData);

// initializing Tabbar for navigation
const tabbar = new dhx.Tabbar(null, {
    views: [
        { id: "kanban", tab: "Kanban" },
        { id: "todo", tab: "To Do List" },
        { id: "gantt", tab: "Gantt" },
        { id: "calendar", tab: "Event Calendar" },
    ],
    tabAlign: "center",
    noContent: true,
});

// attaching widgets into Layout cells
layout.getCell("toolbar").attach(toolbar);
layout.getCell("tabbar").attach(tabbar);

// set theme for toolbar
dhx.setTheme("dark", "toolbar");

// set active view
setActiveView(activeView);
tabbar.events.on("change", id => setActiveView(id));

function login(url) {
    const token = sessionStorage.getItem("login-token");
    if (token) {
        return Promise.resolve(token);
    }
    return fetch(url)
        .then(async (res) => {
            if (res.ok && res.status == 200) {
                return res.text();
            } else {
                sessionStorage.clear();
                return Promise.reject(await res.text());
            }
        })
        .then((token) => {
            sessionStorage.setItem("login-token", token);
            return token;
        })
        .catch((err) => err);
}

function initKanban(token, serverURL) {
    const url = serverURL + "/api/kanban";

    const {
        Kanban,
        Toolbar,
        RestDataProvider,
        RemoteEvents,
        kanbanUpdates,
        defaultEditorShape,
        defaultCardShape
    } = kanban;

    const restProvider = new RestDataProvider(url);
    restProvider.setHeaders({
        "Remote-Token": token
    });
    Promise.all([
        restProvider.getCards(),
        restProvider.getColumns(),
        restProvider.getRows(),
        restProvider.getUsers(),
        restProvider.getLinks(),
    ]).then(([cards, columns, rows, users, links]) => {
        users.forEach((v) => (v.label = v.name));
        const cardShape = {
            ...defaultCardShape,
            label: true,
            description: true,
            progress: true,
            start_date: true,
            end_date: true,
            priority: true,
            color: true,
            cover: true,
            comments: true,
            attached: true,
            votes: {
                show: true
            },
            users: {
                show: true,
                values: users
            }
        };
        const editorShape = [
            ...defaultEditorShape,
            {
                type: "links",
                key: "links",
                label: "Links"
            },
            {
                type: "files", 
                key: "attached", // the "attached" key is used when configuring the "cardShape" property
                label: "Attachment",
                uploadURL: url + "/uploads", // specify url as string
                config: {
                    accept: "image/*", // "video/*", "audio/*"
                    disabled: false,
                    multiple: true,
                    folder: false
                }
            },
            {
                type: "comments",
                key: "comments",
                label: "Comments",
                config: {
                    placement: "editor",
                },
            },
        ];

        const board = new Kanban("#kanban_root", {
            cards,
            columns,
            rows,
            rowKey: "row",
            cardShape,
            editorShape,
            history: false,
            currentUser: 1,
            links,
        });

        new Toolbar("#kanban_toolbar", {
            api: board.api,
            items: [
                "search",
                "spacer",
                "sort",
                "addColumn",
                "addRow"
            ],
        });

        board.api.setNext(restProvider);

        const events = new RemoteEvents(url + "/v1", token);
        const handlers = kanbanUpdates(
            board.api,
            restProvider.getIDResolver()
        );
        events.on(handlers);
    });
}

function initTodo(token, serverURL) {
    const url = serverURL + "/api/todo";

    const {
        ToDo,
        Toolbar,
        RestDataProvider,
        todoUpdates,
        RemoteEvents
    } = todo;

    const restProvider = new RestDataProvider(url);
    restProvider.setHeaders({
        "Remote-Token": token
    });

    const fetchProjectTasks = true;
    let activeProject = 0;

    Promise.all([
        restProvider.getUsers(),
        restProvider.getProjects(),
        restProvider.getTags()
    ])
        .then(([users, projects, tags]) => {
            if (projects && projects.length > 0) {
                activeProject = projects[0].id;
                const fetchTasks = fetchProjectTasks
                    ? () => restProvider.getProjectTasks(activeProject)
                    : () => restProvider.getTasks();

                return fetchTasks().then((tasks) => {
                    return [tasks, users, projects, tags];
                });
            }
            return Promise.resolve([[], users, projects, tags]);
        })
        .then(([tasks, users, projects, tags]) => {
            const list = new ToDo("#todo_root", {
                tasks,
                users,
                projects,
                tags,
                activeProject,
                history: false,
                taskShape: {
                    completed: {
                        behavior: "manual" // "auto" by default
                    }
                }
            });
            new Toolbar("#todo_toolbar", {
                api: list.api
            });

            // save data from client to server
            list.api.setNext(restProvider);
            restProvider.setAPI(list.api);

            // get updates from server to client
            const handlers = todoUpdates(
                list.api,
                restProvider.getIDResolver()
            );
            const events = new RemoteEvents(url + "/v1", token);
            events.on(handlers);

            // disable multiselect
            list.api.intercept("select-task", ({ join }) => {
                return !join;
            });
        });
}

function initGantt(token, serverURL) {
    const apiURL = serverURL + "/api/gantt";
    const wsURL = apiURL + "/v1";

    Promise.all([
        fetchData(apiURL + "/tasks"),
        fetchData(apiURL + "/links")
    ]).then(([data, links]) => {
        init(token, [data, links]);
    });

    function init(token, [tasks, links]) {
        gantt.i18n.setLocale({
            labels: {
                time_enable_button: "Schedule",
                time_disable_button: "Unschedule",
            },
        });

        gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
        gantt.config.cascade_delete = false;
        gantt.attachEvent("onBeforeRowDragEnd", (id, parent, tindex) => {
            gantt.getTask(id).tindex = tindex;
            return true;
        });

        gantt.init("gantt_root");
        gantt.config.lightbox.sections = [
            {
                name: "description",
                height: 38,
                map_to: "text",
                type: "textarea",
                focus: true,
            },
            { name: "type", type: "typeselect", map_to: "type" },
            {
                name: "time",
                map_to: "auto",
                button: true,
                type: "duration_optional",
            },
        ];
        gantt.attachEvent("onLightboxSave", function (id, task, is_new) {
            console.log("save", task);
            task.unscheduled = !task.start_date;
            return true;
        });

        const remoteData = {
            data: prepareTasks(tasks),
            links
        };
        gantt.parse(remoteData);

        const dp = gantt.createDataProcessor({
            url: apiURL
        });
        dp.setTransactionMode({
            mode: "REST-JSON",
            headers: {
                "Remote-Token": token
            }
        });

        const remoteEvents = new gantt.RemoteEvents(wsURL, token);
        remoteEvents.on(ganttUpdates(gantt));
    }

    function ganttUpdates(gantt) {
        function taskUpdates(obj) {
            const task = prepareTask(obj.data);
            switch (obj.type) {
                case "add-task":
                    gantt.silent(function () {
                        checkUnscheduled(task);
                        gantt.addTask(task, task.parent);
                    });
                    break;
                case "update-task":
                    gantt.silent(function () {
                        if (!checkUnscheduled(task)) {
                            task.unscheduled = false;
                            task.end_date = gantt.calculateEndDate(
                                task.start_date,
                                task.duration
                            );
                        }
                        gantt.updateTask(task.id, task);
                    });
                    break;
                case "move-task":
                    gantt.silent(function () {
                        gantt.moveTask(task.id, task.index, task.parent);
                    });
                    break;
                case "delete-task":
                    gantt.silent(function () {
                        if (obj.ids?.length) {
                            for (let i = 0; i < obj.ids.length; i++) {
                                gantt.deleteTask(obj.ids[i]);
                            }
                        } else {
                            if (gantt.isTaskExists(task.id))
                                gantt.deleteTask(task.id);
                        }
                    });
                    break;
            }
            if (activeView === "gantt") gantt.render();
        }
        function linkUpdates(obj) {
            switch (obj.type) {
                case "add-link":
                    gantt.silent(function () {
                        gantt.addLink(obj.data);
                    });
                    break;
                case "delete-link":
                    gantt.silent(function () {
                        gantt.deleteLink(obj.data.id);
                    });
                    break;
            }
            if (activeView === "gantt") gantt.render();
        }
        return {
            tasks: taskUpdates,
            links: linkUpdates
        };
    }
    function fetchData(url) {
        return fetch(url)
            .then((data) => {
                return data.json();
            })
            .catch((err) => {
                throw new Error(err);
            });
    }
    function prepareTasks(tasks) {
        if (!tasks || !tasks.length) return [];
        for (let i = 0; i < tasks.length; i++) {
            prepareTask(tasks[i]);
        }
        return tasks;
    }
    function prepareTask(task) {
        if (!task) return null;
        if (!checkUnscheduled(task)) {
            task.start_date = new Date(task.start_date);
        }
        task.open = true;

        return task;
    }
    function checkUnscheduled(task) {
        if (!task.start_date) {
            task.unscheduled = true;
            task.start_date = null;
            task.end_date = null;
            task.duration = null;
            return true;
        }
        return false;
    }
}

function initCalendar(token, serverURL) {
    const { EventCalendar, RestDataProvider, RemoteEvents, remoteUpdates } = eventCalendar;
    const { format } = dateFns;
    const url = serverURL + "/api/scheduler";
    const editorShape = [...eventCalendar.defaultEditorShape];

    function weekEvent({ event, calendar }) {
        const start_date = format(event.start_date, "HH:mm");
        const end_date = format(event.end_date, "HH:mm");
        return `
            <div class="week_event_wrapper">
                <div>${event.text}</div>
                <div>
                    ${start_date}
                    -
                    ${end_date}
                </div>
            </div>
        `;
    }

    const restProvider = new RestDataProvider(url);
    restProvider.setHeaders({
        "Remote-Token": token,
    });
    Promise.all([
        restProvider.getEvents(),
        restProvider.getCalendars(),
    ]).then(([events, calendars]) => {
        const calendar = new EventCalendar("#calendar_root", {
            events,
            calendars,
            date: new Date(),
            editorShape,
            templates: { weekEvent },
        });
        calendar.api.setNext(restProvider);
        // get updates from server to client
        const remoteEvents = new RemoteEvents(url + "/v1", token);
        const handlers = remoteUpdates(calendar.api, restProvider.getIDResolver());
        function parseEvent(e, resolver) {
            if ("id" in e) e.id = resolver(e.id, 1);
            if ("type" in e) e.type = resolver(e.type, 2);
            e.start_date = e.start_date ? new Date(e.start_date) : null;
            e.end_date = e.end_date ? new Date(e.end_date) : null;
            return e;
        }
        function eventUpdate(obj) {
            const event = parseEvent(obj.event, restProvider.getIDResolver());
            checkUnscheduled(event);
            switch (obj.type) {
                case "add-event":
                    calendar.api.exec(obj.type, {
                        id: event.id,
                        event: event,
                        skipProvider: true,
                    });
                    break;
                case "update-event":
                    calendar.api.exec("update-event", {
                        id: event.id,
                        event: event,
                        skipProvider: true,
                    });
                    break;
                case "delete-event":
                    calendar.api.exec("delete-event", {
                        id: event.id,
                        skipProvider: true,
                    });
                    break;
            }
        }
        handlers["events"] = eventUpdate;
        remoteEvents.on(handlers);
    });

    function checkUnscheduled(event) {
        if (!event.start_date || !event.end_date) {
            event.start_date = null;
            event.end_date = null;
        }
    }
}

function setActiveView(id) {
    const views = document.getElementById("views").children;
    for (i = 0; i < views.length; i++) {
        views[i].style.display = views[i].id === id ? "block" : "none";
    }
    activeView = id;
    if (activeView === "gantt") gantt.render();
}
<!-- Other scripts -->
<script
    src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/2.0.0-alpha0/date_fns.min.js"
    integrity="sha512-0kon+2zxkK5yhflwFqaTaIhLVDKGVH0YH/jm8P8Bab/4EOgC/n7gWyy7WE4EXrfPOVDeNdaebiAng0nsfeFd9A=="
    crossorigin="anonymous"
    referrerpolicy="no-referrer"
></script>
<!-- icons -->
<link rel="stylesheet" href="//cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css" />
<!-- component styles -->

<style>
#views, #kanban, #todo, #calendar, #gantt, #gantt_root {
        height: 100%;
        width: 100%;
        background: var(--dhx-background-secondary);
    }
    #kanban_root, #todo_root, #calendar_root {
        height: calc(100% - 56px);
        overflow: auto;
    }
    #todo {
        width: 700px;
        margin: 0 auto;
    }
    #kanban .wx-material-theme {
        --wx-field-width: 100%;
    }
    .toolbar_logo-container {
        height: 55px;
        width: 300px;
    }
    .toolbar_logo-container span {
        margin-left: 20px;
    }
    .toolbar_logo-container a {
        height: 100%;
        width: 100%;
        padding-left: 20px;
        display: flex;
        justify-content: flex-start;
        align-items: center;
        text-decoration: none;
        color: var(--dhx-color-secondary);
        font-weight: var(--dhx-font-weight-medium);
    }
</style>

<script>
const toolbarData = [
        {
            type: "customHTML",
            html: `
                <a href="https://dhtmlx.com/docs/products/javascript-project-management-library/"  target="_blank">
                    <img src="https://dhtmlx.com/docs/products/demoApps/dhtmlxKPI/static/dhx.png" alt="logo" /><span>Project Management Demo</span>  
                </a>`,
            css: "toolbar_logo-container",
        },
        {
            type: "spacer"
        },
        {
            id: "earth",
            icon: "mdi mdi-earth",
            type: "button",
            view: "link",
            color: "secondary",
            circle: true,
        },
        {
            id: "settings",
            icon: "mdi mdi-cog",
            type: "button",
            view: "link",
            color: "secondary",
            circle: true,
        },
        {
            id: "info",
            icon: "mdi mdi-information-outline",
            type: "button",
            view: "link",
            color: "secondary",
            circle: true,
        },
        {
            id: "avatar",
            type: "imageButton",
            src: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_61.jpg"
        }
    ];
</script>

The code uses DHTMLX Suite's Layout and Tabbar for the UI shell. Each widget initializes independently with its own RestDataProvider pointing to different API endpoints under the same server. All four widgets use RemoteEvents and their respective update handlers (kanbanUpdates, todoUpdates, ganttUpdates, custom calendar handlers) to receive real-time updates from other users. Authentication tokens are shared across all providers. The Go backend repository is available as a reference implementation.

Solution overview

  1. Authenticate and obtain a token
  2. Initialize Layout with Toolbar and Tabbar for navigation
  3. Initialize each widget (Kanban, Todo, Gantt, Calendar) with its own RestDataProvider
  4. Connect each widget to the backend with setNext and RemoteEvents
  5. Use a tabbed interface to switch between views

Key points

  • Shared backend, separate APIs: Each widget has its own REST endpoint (/api/kanban, /api/todo, /api/gantt, /api/scheduler). They share a database but have independent API surfaces
  • Real-time sync across widgets: Changes in one widget appear in others via WebSocket. No polling or manual refresh needed
  • Gantt requires manual render on tab switch: Gantt does not redraw itself automatically when hidden. Call gantt.render() when the Gantt tab becomes active
  • Testing multiuser sync: To see real-time updates across tabs, open the demo with different user IDs (?id=1 in one tab, ?id=2 in another). Two tabs with the same user ID won't show each other's changes because the server skips broadcasting back to the originating user
  • Reference backend in Go: The full backend implementation is available at github.com/DHTMLX/project-management-go

Additional resources