JavascriptAdvanced
Event Delegation
Build a dynamic task board that demonstrates event delegation — one listener handling many elements including dynamically added ones.
Event Delegation
Problem
Build a dynamic task board with multiple columns. Tasks can be added, moved between columns, and deleted. All interactions are handled with a single event listener per action using event delegation — not one listener per button.
Page loads
→ Three columns: To Do, In Progress, Done
→ Each column has some example tasks
User adds a task to "To Do"
→ New task appears immediately
→ No new event listeners added
User clicks "→" on a task
→ Task moves to the next column
User clicks "←" on a task
→ Task moves to the previous column
User clicks "✕" on a task
→ Task is deleted from its column
All of this is handled by ONE delegated listener on the boardLogic
- Render the board with three columns and some initial tasks
- Attach ONE click listener on the entire board container
- Read
data-actionanddata-idattributes from the clicked button - Determine which action was clicked — move-left, move-right, or delete
- Find the task by its id
- Perform the action on the tasks data
- Re-render the board
Flow
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Event Delegation Task Board</title>
<style>
body {
font-family: sans-serif;
margin: 40px 20px;
background: #f0f2f5;
}
h1 {
text-align: center;
margin-bottom: 8px;
color: #111;
}
.subtitle {
text-align: center;
color: #888;
font-size: 0.85rem;
margin-bottom: 32px;
}
/* three column board layout */
#board {
display: flex;
gap: 16px;
max-width: 860px;
margin: 0 auto;
align-items: flex-start;
}
/* each column */
.column {
flex: 1;
background: #fff;
border-radius: 12px;
padding: 16px;
min-height: 300px;
}
/* column header */
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.column-title {
font-weight: 700;
font-size: 0.95rem;
color: #333;
}
/* task count badge */
.task-count {
background: #f0f2f5;
color: #888;
font-size: 0.75rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
}
/* add task input row */
.add-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.add-row input {
flex: 1;
padding: 8px 10px;
font-size: 0.9rem;
border: 1.5px solid #ddd;
border-radius: 6px;
outline: none;
}
.add-row input:focus {
border-color: #999;
}
/* add button — data-action="add" data-column is on this */
.add-btn {
padding: 8px 12px;
font-size: 0.9rem;
background: #111;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* individual task card */
.task-card {
background: #f8f9fa;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
}
.task-text {
font-size: 0.9rem;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
}
/* action buttons row on each card */
.task-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
}
.task-actions button {
padding: 3px 8px;
font-size: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
cursor: pointer;
color: #555;
transition: background 0.15s;
}
.task-actions button:hover {
background: #f0f0f0;
}
/* delete button — red on hover */
.task-actions button[data-action="delete"]:hover {
background: #fff0f0;
border-color: #e74c3c;
color: #e74c3c;
}
</style>
</head>
<body>
<h1>Task Board</h1>
<p class="subtitle">
All interactions handled by one delegated listener on the board.
</p>
<!-- board container — ONE click listener is attached here -->
<div id="board"></div>
<script src="script.js"></script>
</body>
</html>Solution
// Step 1 — define columns and initial tasks
const columns = ["To Do", "In Progress", "Done"];
// tasks array — each task has a unique id, text, and column index
let tasks = [
{ id: 1, text: "Research JavaScript closures", column: 0 },
{ id: 2, text: "Write unit tests", column: 0 },
{ id: 3, text: "Build the todo app", column: 1 },
{ id: 4, text: "Review pull request", column: 1 },
{ id: 5, text: "Deploy to Vercel", column: 2 },
];
// counter for generating unique task ids
let nextId = 6;
// Step 2 — select the board container
const board = document.querySelector("#board");
// Step 3 — render the board
render();
// Step 4 — ONE click listener on the entire board
// handles add, move-left, move-right, delete for ALL tasks
// including tasks that do not exist yet
board.addEventListener("click", (e) => {
// find the closest button with a data-action attribute
const btn = e.target.closest("[data-action]");
if (!btn) return; // click was not on an action button
const action = btn.dataset.action; // "add", "move-right", "move-left", "delete"
const taskId = Number(btn.dataset.id); // the task id (not used for "add")
const column = Number(btn.dataset.column); // the column index (used for "add")
// Step 5 — handle each action
if (action === "add") {
// find the input for this column using the column index
const input = document.querySelector(`#input-${column}`);
const text = input.value.trim();
if (!text) return; // do nothing if input is empty
// create new task in the clicked column
tasks.push({ id: nextId++, text, column });
input.value = ""; // clear input
}
if (action === "move-right") {
// find the task and move it to the next column
const task = tasks.find((t) => t.id === taskId);
if (task && task.column < columns.length - 1) {
task.column++;
}
}
if (action === "move-left") {
// find the task and move it to the previous column
const task = tasks.find((t) => t.id === taskId);
if (task && task.column > 0) {
task.column--;
}
}
if (action === "delete") {
// remove the task from the array
tasks = tasks.filter((t) => t.id !== taskId);
}
// Step 6 — re-render after every action
render();
});
// Step 7 — render function — builds the entire board from the tasks array
function render() {
board.innerHTML = columns
.map((columnName, columnIndex) => {
// get tasks for this column
const columnTasks = tasks.filter((t) => t.column === columnIndex);
// build each task card
const taskCards = columnTasks
.map((task) => {
// only show left arrow if not in first column
const leftBtn =
columnIndex > 0
? `<button data-action="move-left" data-id="${task.id}">←</button>`
: "";
// only show right arrow if not in last column
const rightBtn =
columnIndex < columns.length - 1
? `<button data-action="move-right" data-id="${task.id}">→</button>`
: "";
return `
<div class="task-card">
<div class="task-text">${task.text}</div>
<div class="task-actions">
${leftBtn}
${rightBtn}
<button data-action="delete" data-id="${task.id}">✕</button>
</div>
</div>
`;
})
.join("");
// build the full column
return `
<div class="column">
<div class="column-header">
<span class="column-title">${columnName}</span>
<span class="task-count">${columnTasks.length}</span>
</div>
<div class="add-row">
<input
type="text"
id="input-${columnIndex}"
placeholder="Add task..."
/>
<button
class="add-btn"
data-action="add"
data-column="${columnIndex}"
>+</button>
</div>
${taskCards}
</div>
`;
})
.join("");
}Code Execution
Trace through clicking → on task id 1 (currently in column 0):
| Step | Code | Result |
|---|---|---|
| Click fires | e.target | <button data-action="move-right" data-id="1"> |
| Find btn | e.target.closest("[data-action]") | the button |
| Read action | btn.dataset.action | "move-right" |
| Read id | Number(btn.dataset.id) | 1 |
| Find task | tasks.find(t => t.id === 1) | { id:1, text:"...", column: 0 } |
Check column < 2 | 0 < 2 | true |
| Move | task.column++ | column becomes 1 |
| Re-render | render() | task now appears in "In Progress" |
Trace through adding a task to column 0:
| Step | Code | Result |
|---|---|---|
| Click fires | data-action="add" button | — |
| Read column | btn.dataset.column | "0" → 0 |
| Read input | input-0 value | "New task" |
| Push | tasks.push({ id: 6, text: "New task", column: 0 }) | added |
| Clear input | input.value = "" | input emptied |
| Re-render | task appears in To Do | ✅ |
Trace through deleting task id 5:
| Step | Code | Result |
|---|---|---|
| Click fires | data-action="delete" data-id="5" | — |
| Filter | tasks.filter(t => t.id !== 5) | task 5 removed |
| Re-render | task gone from Done column | ✅ |
Output
Page loads
→ To Do: 2 tasks, In Progress: 2 tasks, Done: 1 task
Click → on "Research JavaScript closures"
→ moves to "In Progress" column
Click ✕ on "Deploy to Vercel"
→ removed from "Done" column
Add "Fix CSS bugs" to "To Do"
→ appears immediately with move and delete buttons
→ NO new event listeners added