jQuery & DOM Manipulation

Event Delegation

15 min Lesson 21 of 30

Understanding Event Delegation

Event delegation is a powerful pattern that allows you to handle events for dynamically added elements efficiently. Instead of attaching event handlers to individual elements, you attach one handler to a parent element.

Why Event Delegation?

  • Dynamic content: Works with elements added after page load
  • Performance: Fewer event handlers = better memory usage
  • Simplicity: One handler instead of many
  • Maintenance: Easier to manage and update

The Problem: Direct Event Binding

This Won't Work for New Elements:
<ul id="itemList">
    <li>Item 1</li>
    <li>Item 2</li>
</ul>
<button id="addItem">Add Item</button>

<script>
// This only works for existing items
$("li").on("click", function() {
    alert($(this).text());
});

// Add new item
$("#addItem").on("click", function() {
    $("#itemList").append("<li>New Item</li>");
    // ❌ New items won't respond to clicks!
});
</script>
Problem: Event handlers attached with $(selector).on("event", handler) only bind to elements that exist at the time the code runs.

The Solution: Event Delegation

Using Event Delegation:
<ul id="itemList">
    <li>Item 1</li>
    <li>Item 2</li>
</ul>
<button id="addItem">Add Item</button>

<script>
// Attach handler to parent element
$("#itemList").on("click", "li", function() {
    alert($(this).text());
});

// Add new items
let count = 3;
$("#addItem").on("click", function() {
    $("#itemList").append("<li>Item " + count + "</li>");
    count++;
    // ✓ New items automatically respond to clicks!
});
</script>

Event Delegation Syntax

Delegation Pattern:
// $(parent).on(event, childSelector, handler)

// Examples:
$("#container").on("click", ".button", function() {
    // Handle clicks on .button elements inside #container
});

$(document).on("change", "select.category", function() {
    // Handle changes on select.category elements anywhere in document
});

$(".list").on("mouseenter", "li", function() {
    // Handle mouseenter on li elements inside .list
});
Best Practice: Use the closest stable parent element, not always document. This improves performance by reducing the event's bubble path.

How Event Delegation Works

Event delegation leverages event bubbling - when an event occurs on an element, it bubbles up through its ancestors:

Event Bubbling Example:
<div id="outer">
    <div id="middle">
        <button id="inner">Click Me</button>
    </div>
</div>

<script>
// When button is clicked, event bubbles:
// 1. button#inner
// 2. div#middle
// 3. div#outer
// 4. body
// 5. html
// 6. document
// 7. window

$("#outer").on("click", function(event) {
    console.log("Outer clicked");
    console.log("Target:", event.target.id);        // The element that triggered
    console.log("CurrentTarget:", event.currentTarget.id); // The element handling
});
</script>

Practical Example: Dynamic To-Do List

Complete To-Do List with Event Delegation:
<div id="todoApp">
    <input type="text" id="todoInput" placeholder="Enter task...">
    <button id="addTodo">Add Task</button>
    <ul id="todoList"></ul>
</div>

<style>
.todo-item {
    display: flex;
    align-items: center;
    padding: 10px;
    margin: 5px 0;
    background: var(--bg-light);
    border-radius: 5px;
}
.todo-item.completed {
    text-decoration: line-through;
    opacity: 0.6;
}
.todo-item button {
    margin-left: auto;
    padding: 5px 10px;
    cursor: pointer;
}
</style>

<script>
$(document).ready(function() {
    // Add new todo
    $("#addTodo").on("click", function() {
        let text = $("#todoInput").val().trim();

        if (text !== "") {
            let todoHtml = `
                <li class="todo-item">
                    <input type="checkbox" class="todo-checkbox">
                    <span class="todo-text">${text}</span>
                    <button class="edit-btn">Edit</button>
                    <button class="delete-btn">Delete</button>
                </li>
            `;
            $("#todoList").append(todoHtml);
            $("#todoInput").val("");
        }
    });

    // Enter key to add
    $("#todoInput").on("keypress", function(event) {
        if (event.key === "Enter") {
            $("#addTodo").click();
        }
    });

    // ✓ Event Delegation for checkbox (toggle complete)
    $("#todoList").on("change", ".todo-checkbox", function() {
        $(this).closest(".todo-item").toggleClass("completed");
    });

    // ✓ Event Delegation for delete button
    $("#todoList").on("click", ".delete-btn", function() {
        $(this).closest(".todo-item").fadeOut(300, function() {
            $(this).remove();
        });
    });

    // ✓ Event Delegation for edit button
    $("#todoList").on("click", ".edit-btn", function() {
        let todoItem = $(this).closest(".todo-item");
        let textSpan = todoItem.find(".todo-text");
        let currentText = textSpan.text();

        let newText = prompt("Edit task:", currentText);
        if (newText !== null && newText.trim() !== "") {
            textSpan.text(newText);
        }
    });
});
</script>

Multiple Event Types with Delegation

Handling Multiple Events:
<div id="gallery"></div>
<button id="addImage">Add Image</button>

<script>
// Add images dynamically
let imageCount = 1;
$("#addImage").on("click", function() {
    let imgHtml = `<img src="image${imageCount}.jpg" class="gallery-img">`;
    $("#gallery").append(imgHtml);
    imageCount++;
});

// Multiple delegated events
$("#gallery")
    .on("mouseenter", ".gallery-img", function() {
        $(this).css("transform", "scale(1.1)");
    })
    .on("mouseleave", ".gallery-img", function() {
        $(this).css("transform", "scale(1)");
    })
    .on("click", ".gallery-img", function() {
        alert("Image clicked: " + $(this).attr("src"));
    })
    .on("dblclick", ".gallery-img", function() {
        $(this).remove();
    });
</script>

Delegation with Event Namespacing

Using Event Namespaces:
// Add namespaced events
$("#container").on("click.myNamespace", ".button", function() {
    console.log("Button clicked");
});

$("#container").on("mouseenter.myNamespace", ".button", function() {
    console.log("Button hover");
});

// Remove only namespaced events
$("#container").off(".myNamespace");

// Specific namespace event
$("#container").off("click.myNamespace");

Performance Comparison

Direct Binding vs Delegation:
// ❌ Bad: Direct binding for many elements
$(".item").on("click", function() {
    // 1000 items = 1000 event handlers in memory
});

// ✓ Good: Event delegation
$("#itemList").on("click", ".item", function() {
    // 1000 items = 1 event handler in memory
});
Performance Tip: For large lists (100+ items), event delegation can improve performance by 10-50x and significantly reduce memory usage.

Stopping Event Propagation

Controlling Event Bubbling:
<div id="parent">
    Parent
    <div id="child">
        Child
        <button id="button">Button</button>
    </div>
</div>

<script>
$("#parent").on("click", function() {
    console.log("Parent clicked");
});

$("#child").on("click", function(event) {
    console.log("Child clicked");
    // Prevent event from bubbling to parent
    event.stopPropagation();
});

$("#button").on("click", function(event) {
    console.log("Button clicked");
    // This stops all propagation
    event.stopImmediatePropagation();
});
</script>
Caution: Use stopPropagation() sparingly as it can break event delegation patterns higher up in the DOM tree.

Advanced Example: Nested Comments System

Comment System with Delegation:
<div id="comments">
    <div class="comment" data-id="1">
        <p>First comment</p>
        <button class="reply-btn">Reply</button>
        <button class="like-btn">Like (<span class="like-count">0</span>)</button>
        <div class="replies"></div>
    </div>
</div>

<script>
let commentIdCounter = 2;

// Delegate: Reply button
$("#comments").on("click", ".reply-btn", function() {
    let comment = $(this).closest(".comment");
    let replies = comment.find("> .replies");

    let replyText = prompt("Enter your reply:");
    if (replyText) {
        let replyHtml = `
            <div class="comment reply" data-id="${commentIdCounter++}"
                 style="margin-left: 30px;">
                <p>${replyText}</p>
                <button class="reply-btn">Reply</button>
                <button class="like-btn">Like (<span class="like-count">0</span>)</button>
                <div class="replies"></div>
            </div>
        `;
        replies.append(replyHtml);
    }
});

// Delegate: Like button
$("#comments").on("click", ".like-btn", function() {
    let likeCount = $(this).find(".like-count");
    let count = parseInt(likeCount.text());
    likeCount.text(count + 1);

    $(this).prop("disabled", true).css("opacity", "0.5");
});

// Delegate: Show comment ID on hover
$("#comments").on("mouseenter", ".comment", function() {
    let id = $(this).data("id");
    $(this).css("background-color", "var(--bg-light)");
    console.log("Hovering comment ID:", id);
}).on("mouseleave", ".comment", function() {
    $(this).css("background-color", "transparent");
});
</script>

Delegation with Custom Data Attributes

Using Data Attributes:
<div id="productList">
    <div class="product" data-id="101" data-price="29.99">
        Product 1
        <button class="add-to-cart">Add to Cart</button>
    </div>
    <div class="product" data-id="102" data-price="49.99">
        Product 2
        <button class="add-to-cart">Add to Cart</button>
    </div>
</div>
<div id="cart">Cart: $0.00</div>

<script>
let cartTotal = 0;

$("#productList").on("click", ".add-to-cart", function() {
    let product = $(this).closest(".product");
    let productId = product.data("id");
    let productPrice = parseFloat(product.data("price"));

    cartTotal += productPrice;
    $("#cart").text("Cart: $" + cartTotal.toFixed(2));

    console.log("Added product ID:", productId);
});
</script>

Practice Exercise:

Task: Build a dynamic table management system with event delegation:

  1. Create a table with "Name", "Email", "Actions" columns
  2. Add button to insert new rows dynamically
  3. Each row has "Edit", "Delete", and "Duplicate" buttons
  4. Edit button: Make row editable inline
  5. Delete button: Remove row with fade animation
  6. Duplicate button: Create copy of row below current
  7. Add row highlighting on hover
  8. Add click counter for each row

Bonus: Implement drag-and-drop row reordering using event delegation. Add undo functionality for deletions.

Common Pitfalls

Mistakes to Avoid:
// ❌ Wrong: Using this in arrow function
$("#list").on("click", "li", () => {
    $(this).addClass("active"); // 'this' is undefined!
});

// ✓ Correct: Use regular function
$("#list").on("click", "li", function() {
    $(this).addClass("active");
});

// ❌ Wrong: Too generic delegation
$(document).on("click", "div", function() {
    // Triggers for ALL divs in document!
});

// ✓ Correct: Specific parent and selector
$("#container").on("click", ".specific-div", function() {
    // Only triggers for .specific-div inside #container
});
</script>

Key Takeaways

  • Event delegation works with dynamically added elements
  • Syntax: $(parent).on(event, childSelector, handler)
  • Uses event bubbling to handle events on ancestors
  • Better performance for large numbers of elements
  • Choose the closest stable parent, not always document
  • Use regular functions, not arrow functions, when you need this