jQuery & DOM Manipulation
Event Delegation
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:
- Create a table with "Name", "Email", "Actions" columns
- Add button to insert new rows dynamically
- Each row has "Edit", "Delete", and "Duplicate" buttons
- Edit button: Make row editable inline
- Delete button: Remove row with fade animation
- Duplicate button: Create copy of row below current
- Add row highlighting on hover
- 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