Skip to main content

DHTMLX. Kanban and To do list with client-side sync example

A To Do list provides a flat, checkbox-driven task view, while a Kanban board organizes the same tasks visually by status. This demo synchronizes both widgets bidirectionally: checking off a task in the To Do list moves it to "Done" on the Kanban, and dragging a card between columns updates the task's status in the list.

Live example

// Kanban initialization

const board = new kanban.Kanban("#kanbanRoot", {
    cards: tasks.data,
    columns: tasks.columns,
    rows: tasks.projects,
    rowKey: "project",
    cardShape,
    editorShape: [
        ...kanban.defaultEditorShape,
        {
            type: "select",
            label: "Column",
            key: "column",
            options: tasks.columns,
        },
        {
            type: "select",
            label: "Project",
            key: "project",
            options: tasks.projects,
        },
        ...editorShape,
    ],
});
new kanban.Toolbar("#kanbanToolbar", {
    api: board.api,
});

// To Do List initialization

const list = new todo.ToDo("#todoRoot", {
    tasks: getToDoData(tasks.data),
    users: getToDoUsers(tasks.users),
    projects: tasks.projects,
    tags: ["high", "medium", "low"]
});
new todo.Toolbar("#todoToolbar", {
    api: list.api,
});

// Kanban events
let skipKanban = false;

board.api.on("select-card", ({ id }) => {
    skipKanban = true;
    list.selectTask({ id });
});
board.api.on("unselect-card", ({ id }) => {
    skipKanban = true;
    list.unselectTask({ id });
});
board.api.on("add-card", ({ id, rowId, card }) => {
    skipKanban = true;
    !list.existsTask({ id }) && list.addTask({
        id,
        project: rowId,
        task: getListTaskTransform(card),
    });
});
board.api.on("delete-card", ({ id }) => {
    skipKanban = true;
    list.deleteTask({ id });
});
board.api.on("update-card", ({ id, card }) => {
    skipKanban = true;
    list.updateTask({
        id,
        task: getListTaskTransform(card),
    });
});
board.api.on("move-card", ({ id, rowId }) => {
    skipKanban = true;
    const card = board.getCard(id);

    if (rowId !== card.project) {
        list.moveTask({
            id,
            project: rowId,
        });
    } else {
        list.updateTask({
            id,
            task: getListTaskTransform(card),
        });
    }
});
board.api.on("add-row", ({ id, row }) => {
    skipKanban = true;
    list.addProject({
        id,
        project: row,
    });
});
board.api.on("delete-row", ({ id }) => {
    skipKanban = true;
    list.deleteProject({ id });
});
board.api.on("update-row", ({ id, row }) => {
    skipKanban = true;
    list.updateProject({ id, project: row });
});

// To Do List events

list.api.on("select-task", ({ id }) => {
    if (skipKanban) return skipKanban = false;
    board.selectCard({ id });
});
list.api.on("unselect-task", ({ id }) => {
    if (skipKanban) return skipKanban = false;
    board.unselectCard({ id });
});
list.api.on("add-task", ({ id }) => {
    if (skipKanban) return skipKanban = false;
    const task = list.getTask({ id });
    board.addCard({
        id,
        card: {
            label: "",
            ...getNextDateRange(3),
            ...getKanbanCardTransform(task)
        },
        rowId: task?.project,
    });
});
list.api.intercept("delete-task", ({ id }) => {
    if (skipKanban) {
        skipKanban = false;
        return;
    }
    const children = list.getChildrenIds({ id });
    for (const childId of children) {
        board.deleteCard({ id: childId });
    }
    board.deleteCard({ id });
});
list.api.on("update-task", ({ id, task }) => {
    if (skipKanban) return skipKanban = false;

    const card = board.getCard(id);
    board.updateCard({
        id,
        card: {
            ...card,
            ...getKanbanCardTransform(task),
        }
    });
});
list.api.on("add-project", ({ id, project }) => {
    if (skipKanban) return skipKanban = false;

    board.addRow({
        id,
        row: project,
    });
});
list.api.intercept("delete-project", ({ id }) => {
    if (skipKanban) return skipKanban = false;

    const { tasks, projects } = list.serialize();
    const projectTasks = tasks.filter(task => task.project === id && !task.parent );
    for (const task of projectTasks) {
        list.deleteTask({ id: task.id });
    }

    const nextProject = projects.filter(p => p.id !== id)[0];
    list.setProject({ id: nextProject?.id });

    board.deleteRow({ id });
});
list.api.on("update-project", ({ id, project }) => {
    if (skipKanban) return skipKanban = false;

    board.updateRow({
        id,
        row: project,
    });
});

// Common methods

function getToDoUsers(data) {
    const tasks = JSON.parse(JSON.stringify(data));
    for (let index = 0; index < tasks.length; index++) {
        tasks[index].avatar = tasks[index]?.path;
    }
    return tasks;
}

function getToDoData(data) {
    const tasks = JSON.parse(JSON.stringify(data));
    for (let index = 0; index < tasks.length; index++) {
        tasks[index] = getListTaskTransform(tasks[index]);
    }
    return tasks;
}

function getListTaskTransform(obj) {
    const task = { ...obj };
    if ("end_date" in task) task.due_date = task.end_date;
    if ("users" in task) task.assigned = task.users;
    if ("label" in task) task.text = task.label;
    if (task?.column === "done") {
        task.checked = true;
    } else {
        task.checked = !!(task?.progress === 100);
    }
    return task;
}

function getKanbanCardTransform(obj) {
    const card = { ...obj };
    if ("start_date" in card) card.start_date = new Date(card.start_date);
    if ("end_date" in card) card.end_date = new Date(card.end_date);
    if ("due_date" in card) card.end_date = new Date(card.due_date);
    if ("assigned" in card) card.users = card.assigned;
    if ("text" in card) card.label = card.text;
    if (card.checked && card.column !== "done") {
        card.column = "done";
    } else if (!card.checked && card.column === "done") {
        card.column = "backlog";
    }
    if ("progress" in card) {
        if (card?.checked) {
            card.progress = 100;
        } else if (card.progress === 100) {
            card.progress = 0;
        }
    }
    return card;
}
<main class="sample-container">
    <!-- To Do List container -->
    <section class="widget-container">
        <div id="todoToolbar"></div>
	    <div id="todoRoot" style="height: calc(100% - 56px);"></div>
    </section>
    <!-- Kanban container -->
    <section class="widget-container">
        <div id="kanbanToolbar" style="margin-top: 8px"></div>
        <div id="kanbanRoot" style="height: calc(100% - 64px);"></div>
    </section>
</main>

<style>
.sample-container {
        overflow: hidden;
        height: 100%;
        width: 100%;
    }
    .widget-container {
        height: 50%;
        width: 100%;
    }
</style>

<script>
const date = new Date();
	function getNextDateRange(days) {
		const startDate = new Date(date.valueOf());
		const endDate = new Date(date.setDate(date.getDate() + days));
		return {
			start_date: startDate,
			end_date: endDate,
		};
	}

    const tasks = {
        data: [
            {
                id: 1,
                label: "Integration with Angular/React #high",
                priority: 1,
                color: "#65D3B3",
                users: [3, 2],
                column: "backlog",
                project: "feature",
                ...getNextDateRange(3),
            },
            {
                id: 2,
                label: "Archive the cards/boards #low",
                priority: 3,
                color: "#58C3FE",
                users: [5],
                progress: 0,
                column: "backlog",
                project: "feature",
                ...getNextDateRange(3),
            },
            {
                id: 3,
                label: "Searching and filtering #high",
                priority: 1,
                color: "#58C3FE",
                users: [3, 1],
                progress: 0,
                column: "backlog",
                project: "task",
                ...getNextDateRange(2),
            },
            {
                id: 4,
                label: "Set the tasks priorities",
                color: "#FFC975",
                users: [4],
                progress: 75,
                column: "inprogress",
                project: "feature",
                attached: [
                    {
                        isCover: true,
                        coverURL: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-1.jpg",
                        previewURL: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-1.jpg",
                        url: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-1.jpg",
                        name: "img-1.jpg",
                    },
                ],
                ...getNextDateRange(3),
            },
            {
                id: 5,
                label: "Custom icons",
                color: "#65D3B3",
                users: [3, 2],
                column: "inprogress",
                project: "task",
                ...getNextDateRange(4),
            },
            {
                id: 6,
                label: "Integration with Gantt #medium",
                color: "#FFC975",
                users: [4],
                progress: 75,
                column: "inprogress",
                project: "task",
                ...getNextDateRange(3),
            },
            {
                id: 7,
                label: "Drag and drop #high",
                priority: 1,
                color: "#58C3FE",
                users: [3, 1],
                progress: 100,
                column: "testing",
                project: "feature",
                ...getNextDateRange(1),
            },
            {
                id: 8,
                label: "Adding images",
                color: "#58C3FE",
                users: [4],
                column: "testing",
                project: "task",
                attached: [
                    {
                        isCover: true,
                        coverURL: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-2.jpg",
                        previewURL: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-2.jpg",
                        url: "https://snippet.dhtmlx.com/codebase/data/kanban/01/img/img-2.jpg",
                        name: "img-2.jpg",
                    },
                ],
                ...getNextDateRange(3),
            },
            {
                id: 9,
                label: "Create cards and lists from the UI and from code #low",
                priority: 3,
                color: "#65D3B3",
                users: [3, 2],
                column: "done",
                project: "feature",
                ...getNextDateRange(3),
            },
            {
                id: 10,
                label: "Draw swimlanes #medium",
                color: "#FFC975",
                users: [2],
                column: "done",
                project: "feature",
                ...getNextDateRange(3),
            },
            {
                id: 11,
                label: "Progress bar #high",
                priority: 1,
                color: "#FFC975",
                users: [1, 4, 3],
                progress: 100,
                column: "done",
                project: "task",
                ...getNextDateRange(3),
            },
        ],
        projects: [
            { id: "feature", label: "Feature" },
            { id: "task", label: "Task" },
        ],
        users: [
            {
                id: 1,
                label: "John",
                path: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_61.jpg",
            },
            {
                id: 2,
                label: "Nadia",
                path: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_63.jpg",
            },
            {
                id: 3,
                label: "Mike",
                path: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_03.jpg",
            },
            {
                id: 4,
                label: "Elvira",
                path: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_33.jpg",
            },
            {
                id: 5,
                label: "Floe",
                path: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_35.jpg",
            },
        ],
        columns: [
            {
                label: "Backlog",
                id: "backlog",
            },
            {
                label: "In progress",
                id: "inprogress",
            },
            {
                label: "Testing",
                id: "testing",
            },
            {
                label: "Done",
                id: "done",
            },
        ]
    };

    const cardShape = {
		label: true,
		description: true,
		progress: true,
		start_date: true,
		end_date: true,
		users: {
			show: true,
			values: tasks.users,
		},
		priority: {
			show: true,
			values: [
				{ id: 1, color: "#FF5252", label: "High", value: 1 },
				{ id: 2, color: "#FFC975", label: "Medium", value: 2 },
				{ id: 3, color: "#65D3B3", label: "Low", value: 3 },
			],
		},
		color: true,
		menu: true,
		cover: true,
		attached: false,
      	column: true,
		project: true,
	};
    
    const editorShape = [
		{
			project: "multiselect",
			key: "users",
			label: "Users",
			options: tasks.users,
		},
	];
</script>

The code initializes both widgets with the same data (transformed between formats) and wires up extensive event handlers. A skipKanban flag prevents re-entrant loops: when the Kanban fires an event, skipKanban is set to true so the corresponding To Do handler skips the update. Data transforms handle field mapping. Kanban's label/users/end_date become To Do's text/assigned/due_date, and the checked state is derived from column position and progress. The intercept method is used for delete operations to handle cascading deletes of child tasks.

Solution overview

  1. Initialize Kanban and To Do List with the same dataset (transformed between formats)
  2. Listen to CRUD events on both widgets
  3. Use skipKanban flag to prevent infinite update loops
  4. Transform data fields between widgets (label/text, users/assigned, end_date/due_date)
  5. Handle special cases: checked ↔ column mapping, cascading deletes, project syncing

Key points

  • Bidirectional sync requires loop guards: The skipKanban flag prevents updates from bouncing between widgets infinitely
  • Field mapping is critical: Kanban and To Do use different field names for the same data. getListTaskTransform and getKanbanCardTransform handle the conversions
  • Checked/column coupling: Checking a To Do task moves the Kanban card to "Done"; unchecking moves it to "Backlog". Moving a card to "Done" checks the task
  • intercept for deletes: Using intercept instead of on for delete events lets you handle child tasks before the parent is removed

Additional resources