Docs
17.4-Event-Delegation
10.4 Event Delegation
📚 Learning Objectives
By the end of this section, you will:
- •Understand what event delegation is and why it's useful
- •Use event delegation for dynamic content
- •Handle events on child elements through parent listeners
- •Implement common delegation patterns
- •Optimize event handling for performance
📖 Table of Contents
- •What is Event Delegation?
- •How Delegation Works
- •Delegation Patterns
- •Dynamic Content Handling
- •Best Practices
What is Event Delegation?
The Problem with Individual Listeners
┌──────────────────────────────────────────────────────────────────┐
│ Without Event Delegation │
├──────────────────────────────────────────────────────────────────┤
│ │
│ <ul id="list"> │
│ <li>Item 1</li> ← addEventListener("click", handler) │
│ <li>Item 2</li> ← addEventListener("click", handler) │
│ <li>Item 3</li> ← addEventListener("click", handler) │
│ <li>Item 4</li> ← addEventListener("click", handler) │
│ <li>Item 5</li> ← addEventListener("click", handler) │
│ ... 100 more items │
│ </ul> │
│ │
│ Problems: │
│ ✗ Memory overhead (100+ handlers) │
│ ✗ Slow initial setup │
│ ✗ New items don't have handlers │
│ ✗ Harder to manage/remove handlers │
│ │
└──────────────────────────────────────────────────────────────────┘
The Solution: Event Delegation
┌──────────────────────────────────────────────────────────────────┐
│ With Event Delegation │
├──────────────────────────────────────────────────────────────────┤
│ │
│ <ul id="list"> ← ONE addEventListener("click", handler) │
│ <li>Item 1</li> │ │
│ <li>Item 2</li> │ Events bubble up │
│ <li>Item 3</li> │ to the parent │
│ <li>Item 4</li> │ │
│ <li>Item 5</li> ↓ │
│ ... any number of items │
│ </ul> │
│ │
│ Benefits: │
│ ✓ Single handler (less memory) │
│ ✓ Fast setup │
│ ✓ Works with dynamic content │
│ ✓ Easy to manage │
│ │
└──────────────────────────────────────────────────────────────────┘
How Delegation Works
Event Bubbling Enables Delegation
┌─────────────────────────────────────────────────────────────────────┐
│ Event Delegation Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. User clicks an item │
│ ┌─────────────────────────────────────────┐ │
│ │ <ul id="list"> ← Handler here │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ <li>Item 1</li> │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ <li>Item 2</li> ← CLICK HERE │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ <li>Item 3</li> │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 2. Event bubbles up to <ul> │
│ 3. Handler checks event.target │
│ 4. If target is an <li>, handle it │
│ │
└─────────────────────────────────────────────────────────────────────┘
Basic Delegation Pattern
// Get the parent container
const list = document.getElementById('list');
// Add single listener to parent
list.addEventListener('click', (event) => {
// Check if clicked element is what we want
if (event.target.tagName === 'LI') {
console.log('Clicked:', event.target.textContent);
}
});
Using closest() for Nested Content
// When list items contain nested elements:
// <li><span class="icon">📁</span> <span class="name">File.txt</span></li>
list.addEventListener('click', (event) => {
// Find the closest <li> ancestor (including self)
const listItem = event.target.closest('li');
if (listItem) {
console.log('Clicked item:', listItem.textContent);
}
});
target vs closest()
┌──────────────────────────────────────────────────────────────────┐
│ event.target vs event.target.closest() │
├──────────────────────────────────────────────────────────────────┤
│ │
│ HTML: │
│ <li class="item"> │
│ <span class="icon">📁</span> │
│ <span class="name">File.txt</span> │
│ </li> │
│ │
│ If user clicks the icon: │
│ │
│ event.target │
│ → <span class="icon">📁</span> (exact element clicked) │
│ │
│ event.target.closest(".item") │
│ → <li class="item">...</li> (nearest matching ancestor) │
│ │
│ Use closest() when: │
│ • Elements have nested children │
│ • You want to find the containing element │
│ • Clicks might land on child elements │
│ │
└──────────────────────────────────────────────────────────────────┘
Delegation Patterns
Pattern 1: Simple Tag Matching
const container = document.getElementById('container');
container.addEventListener('click', (event) => {
if (event.target.tagName === 'BUTTON') {
console.log('Button clicked:', event.target.textContent);
}
});
Pattern 2: Class-Based Matching
const toolbar = document.getElementById('toolbar');
toolbar.addEventListener('click', (event) => {
if (event.target.classList.contains('tool-btn')) {
const action = event.target.dataset.action;
console.log('Tool action:', action);
}
});
Pattern 3: Data Attribute Matching
const menu = document.getElementById('menu');
menu.addEventListener('click', (event) => {
const item = event.target.closest('[data-action]');
if (item) {
const action = item.dataset.action;
handleAction(action);
}
});
function handleAction(action) {
switch (action) {
case 'save':
console.log('Saving...');
break;
case 'delete':
console.log('Deleting...');
break;
case 'edit':
console.log('Editing...');
break;
}
}
Pattern 4: Multiple Element Types
const panel = document.getElementById('panel');
panel.addEventListener('click', (event) => {
const target = event.target;
if (target.matches('button.save')) {
handleSave();
} else if (target.matches('button.cancel')) {
handleCancel();
} else if (target.matches('a.link')) {
event.preventDefault();
handleLink(target.href);
} else if (target.closest('.card')) {
handleCardClick(target.closest('.card'));
}
});
Pattern 5: Action Router
const app = document.getElementById('app');
const actions = {
toggleMenu: () => document.body.classList.toggle('menu-open'),
openModal: (el) => openModal(el.dataset.modalId),
closeModal: () => closeCurrentModal(),
submitForm: (el) => handleFormSubmit(el.closest('form')),
deleteItem: (el) => deleteItem(el.dataset.itemId),
};
app.addEventListener('click', (event) => {
const actionElement = event.target.closest('[data-action]');
if (actionElement) {
const action = actionElement.dataset.action;
if (actions[action]) {
event.preventDefault();
actions[action](actionElement);
}
}
});
Dynamic Content Handling
Adding Items Dynamically
const list = document.getElementById('todoList');
const addButton = document.getElementById('addBtn');
const input = document.getElementById('todoInput');
// Delegation handles all items, even new ones
list.addEventListener('click', (event) => {
const deleteBtn = event.target.closest('.delete-btn');
if (deleteBtn) {
const item = deleteBtn.closest('li');
item.remove();
console.log('Item deleted');
}
});
// Add new items - they automatically work!
addButton.addEventListener('click', () => {
const li = document.createElement('li');
li.innerHTML = `
${input.value}
<button class="delete-btn">Delete</button>
`;
list.appendChild(li);
input.value = '';
});
Before vs After Delegation
┌─────────────────────────────────────────────────────────────────────┐
│ Without Delegation With Delegation │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ // Must add handler each time // Setup once │
│ function addItem(text) { function addItem(text) { │
│ const li = createElement("li"); const li = createElement("li");
│ li.textContent = text; li.textContent = text; │
│ │
│ // Handler for THIS item // No handler needed! │
│ li.addEventListener("click", list.appendChild(li); │
│ handleClick } │
│ ); │
│ // One handler for all │
│ list.appendChild(li); list.addEventListener("click", │
│ } handleClick │
│ ); │
│ ❌ New items need handlers │
│ ❌ Can forget to add handler ✅ Works automatically │
│ ❌ Memory grows with items ✅ Single handler │
│ │
└─────────────────────────────────────────────────────────────────────┘
Real-World Example: Table Actions
const table = document.querySelector('table');
table.addEventListener('click', (event) => {
const target = event.target;
const row = target.closest('tr');
if (!row) return;
const rowId = row.dataset.id;
if (target.matches('.edit-btn')) {
editRow(rowId);
} else if (target.matches('.delete-btn')) {
deleteRow(rowId);
} else if (target.matches('.view-btn')) {
viewDetails(rowId);
}
});
// Rows can be added/removed dynamically
function addRow(data) {
const row = document.createElement('tr');
row.dataset.id = data.id;
row.innerHTML = `
<td>${data.name}</td>
<td>${data.email}</td>
<td>
<button class="view-btn">View</button>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</td>
`;
table.querySelector('tbody').appendChild(row);
}
Best Practices
Do's and Don'ts
┌──────────────────────────────────────────────────────────────────┐
│ Event Delegation Best Practices │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ✅ DO: │
│ • Use delegation for lists, tables, grids │
│ • Use closest() for nested elements │
│ • Use data attributes for action identification │
│ • Add listener to nearest stable ancestor │
│ • Check target before acting │
│ │
│ ❌ DON'T: │
│ • Add listener to document for everything │
│ • Forget to check event.target │
│ • Use delegation for single, static elements │
│ • Delegate events that don't bubble (focus, blur) │
│ │
└──────────────────────────────────────────────────────────────────┘
Events That Don't Bubble
// These events DON'T bubble - use capture phase instead:
// focus, blur, load, unload, scroll (on elements)
// ❌ Won't work with delegation:
container.addEventListener('focus', handler); // focus doesn't bubble
// ✓ Use focusin/focusout instead (they bubble):
container.addEventListener('focusin', handler);
container.addEventListener('focusout', handler);
// ✓ Or use capture phase:
container.addEventListener('focus', handler, true); // capture phase
Performance Considerations
// ❌ Too broad - document handles ALL clicks
document.addEventListener('click', handler);
// ✓ Better - scope to specific container
document.getElementById('myApp').addEventListener('click', handler);
// ✓ Even better - scope to immediate parent
document.getElementById('itemList').addEventListener('click', handler);
Delegation Decision Flow
┌─────────────────────────────────────────────────────────────────────┐
│ When to Use Event Delegation? │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ Multiple similar │ │
│ │ elements? │ │
│ └─────────┬───────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ┌──────────┴──────────┐ ┌─────────┴─────────┐ │
│ │ Dynamic content? │ │ Single static │ │
│ │ (items add/remove) │ │ element? Use │ │
│ └─────────┬───────────┘ │ direct listener │ │
│ │ └───────────────────┘ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ Use delegation Consider delegation │
│ (strongly for cleaner code │
│ recommended) and less memory │
│ │
└─────────────────────────────────────────────────────────────────────┘
Common Patterns Summary
| Pattern | When to Use | Example |
|---|---|---|
| Tag matching | Simple lists of same element | event.target.tagName === "LI" |
| Class matching | Elements with specific classes | event.target.classList.contains("btn") |
closest() | Nested content, complex structure | event.target.closest(".card") |
matches() | Complex CSS selectors | event.target.matches("button.primary") |
| Data attributes | Action-based handling | event.target.dataset.action |
💡 Key Takeaways
- •Single Handler: Attach one listener to a parent instead of many to children
- •Use
closest(): Find the meaningful element when content is nested - •Check Targets: Always verify the clicked element before acting
- •Dynamic Content: Delegation automatically handles new elements
- •Scope Appropriately: Use the nearest stable ancestor, not document
🔗 Next Steps
Continue to 10.5 Forms and User Input to learn how to handle form submissions and user input effectively.