DocsHub
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 board

Logic

  1. Render the board with three columns and some initial tasks
  2. Attach ONE click listener on the entire board container
  3. Read data-action and data-id attributes from the clicked button
  4. Determine which action was clicked — move-left, move-right, or delete
  5. Find the task by its id
  6. Perform the action on the tasks data
  7. Re-render the board

Flow

no yes delete move-right move-left add User clicks anything on the board event bubbles up to board container Click on a buttonwith data-action? Ignore click Read data-action and data-id Which action? Remove task from array Move task to next column Move task to previous column Push new task to column Re-render board

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):

StepCodeResult
Click firese.target<button data-action="move-right" data-id="1">
Find btne.target.closest("[data-action]")the button
Read actionbtn.dataset.action"move-right"
Read idNumber(btn.dataset.id)1
Find tasktasks.find(t => t.id === 1){ id:1, text:"...", column: 0 }
Check column < 20 < 2true
Movetask.column++column becomes 1
Re-renderrender()task now appears in "In Progress"

Trace through adding a task to column 0:

StepCodeResult
Click firesdata-action="add" button
Read columnbtn.dataset.column"0"0
Read inputinput-0 value"New task"
Pushtasks.push({ id: 6, text: "New task", column: 0 })added
Clear inputinput.value = ""input emptied
Re-rendertask appears in To Do

Trace through deleting task id 5:

StepCodeResult
Click firesdata-action="delete" data-id="5"
Filtertasks.filter(t => t.id !== 5)task 5 removed
Re-rendertask 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

On this page