JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
82: Event Delegation
I want to build a simple interactive shopping list. The goal is straightforward: when you click an item in the list, it should toggle a "completed" class to strike it through. It sounds trivial, but this is where a lot of developers run into a specific wall when their apps start growing.
The "One-by-One" Approach
My first instinct—and probably yours—is to just grab all the list items and slap a listener on each one. Let's try that.
<ul id="shopping-list">
<li>Milk</li>
<li>Eggs</li>
<li>Bread</li>
</ul>
<script>
const items = document.querySelectorAll('#shopping-list li');
items.forEach(item => {
item.addEventListener('click', () => {
item.classList.toggle('completed');
});
});
</script>
This works perfectly for the three items already on the page. I click "Milk," it crosses out. I click "Eggs," it crosses out. Everything seems fine. But here is where the real world hits us: our list is dynamic. We need to be able to add new items.
The Dynamic Element Dilemma
Let's add a simple input and a button to add new groceries to our list. I'll write a quick function to append a new li to the ul.
const list = document.getElementById('shopping-list');
const input = document.getElementById('item-input');
const addButton = document.getElementById('add-btn');
addButton.addEventListener('click', () => {
const newItem = document.createElement('li');
newItem.textContent = input.value;
list.appendChild(newItem);
input.value = '';
});
Now, try it out. Add "Coffee" to the list. Now click on "Coffee." Nothing happens. Why? Because the forEach loop I ran at the start only attached listeners to the elements that existed at the moment the page loaded. The new "Coffee" item was born into a world where no one told it how to react to a click.
I could technically add the event listener inside the addButton function every time I create a new element. But imagine a list with 500 items. That's 500 separate function objects sitting in memory. It's inefficient and, frankly, a pain to manage if you ever need to remove or change those listeners.
Bubbling to the Rescue
Here is the "Aha!" moment. I remember that events in JavaScript don't just hit the target and stop; they "bubble" up. If I click an li, that click event also triggers on the ul, then the body, then the document.
So, instead of putting listeners on every single item, why not put one listener on the parent ul? Let's test this theory.
const list = document.getElementById('shopping-list');
list.addEventListener('click', (event) => {
console.log('The list was clicked!');
});
Now, whenever I click any list item—even the ones I add dynamically—the console logs "The list was clicked!" The parent is catching the event as it bubbles up. This is the core of Event Delegation.
Pinpointing the Target
The problem now is that the parent knows something was clicked, but it doesn't inherently know which item it was. If I click the gap between items or the padding of the ul, I don't want to toggle a "completed" class on the whole list.
We can use event.target to see exactly which element triggered the event. Let's refine the logic:
list.addEventListener('click', (event) => {
// We only care if the clicked element is an LI
if (event.target.tagName === 'LI') {
event.target.classList.toggle('completed');
}
});
This is much cleaner. I have one single listener managing an infinite number of potential items. It doesn't matter when the items are added or removed; the parent is always there to catch the bubble.
One last tip: if your list items get more complex (like having a span or an icon inside the li), event.target might return that inner span instead of the li. In those cases, I usually use event.target.closest('li'). It tells JavaScript: "Find the closest element that matches 'li', starting from the target and moving up the tree." It's a lifesaver for preventing bugs in more complex UIs.
📋 Practical Task
Build a Dynamic Task Board with Delegation
Create a simple Task Board application. You will need an input field and an "Add Task" button that appends tasks to a <ul>.
- Implement Event Delegation on the
<ul>so that clicking any task toggles a.doneclass (style it withtext-decoration: line-through). - Each task
<li>should contain a "Delete" button inside it. - Use the same parent event listener to handle the "Delete" button clicks. Use
event.targetor.closest()to identify if the delete button was clicked, and then remove that specific task from the DOM. - Constraint: You are not allowed to attach event listeners directly to the
<li>or the delete buttons during their creation. Everything must be handled by the parent<ul>.
There are no comments for now.