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
- Initialize Kanban and To Do List with the same dataset (transformed between formats)
- Listen to CRUD events on both widgets
- Use
skipKanbanflag to prevent infinite update loops - Transform data fields between widgets (label/text, users/assigned, end_date/due_date)
- Handle special cases: checked ↔ column mapping, cascading deletes, project syncing
Key points
- Bidirectional sync requires loop guards: The
skipKanbanflag prevents updates from bouncing between widgets infinitely - Field mapping is critical: Kanban and To Do use different field names for the same data.
getListTaskTransformandgetKanbanCardTransformhandle 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
interceptfor deletes: Usinginterceptinstead ofonfor delete events lets you handle child tasks before the parent is removed