Delegation in DOM
Event delegation is essential for handling events on dynamic content efficiently.
The Problem with Direct Listeners
// Adding listeners to each item
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// Problem: dynamically added items won't have listeners!
function addItem(text) {
const li = document.createElement('li');
li.className = 'item';
li.textContent = text;
list.appendChild(li); // No listener on this one!
}
Solution: Event Delegation
// Single listener on parent
const list = document.querySelector('ul');
list.addEventListener('click', (e) => {
// Find the closest .item
const item = e.target.closest('.item');
// Check if it exists and is a child of our list
if (item && list.contains(item)) {
handleClick(item);
}
});
// Now ALL items work, including future ones!
function addItem(text) {
const li = document.createElement('li');
li.className = 'item';
li.textContent = text;
list.appendChild(li); // Automatically handled!
}
Why closest() is Better Than target
// BAD: Using target directly
list.addEventListener('click', (e) => {
if (e.target.classList.contains('item')) {
// Only works if clicking directly on .item
// Not on children of .item!
}
});
// GOOD: Using closest()
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item && list.contains(item)) {
// Works even if clicking on children of .item
}
});
Practical Example
// Todo list with delegation
document.getElementById('todo-list').addEventListener('click', (e) => {
const todo = e.target.closest('.todo');
if (!todo) return;
if (e.target.closest('.delete-btn')) {
todo.remove();
} else if (e.target.closest('.edit-btn')) {
editTodo(todo);
} else {
todo.classList.toggle('completed');
}
});
Event Target
Understanding event.target is crucial for effective delegation.
event.target Properties
list.addEventListener('click', (e) => {
// The element that triggered the event
console.log('Target:', e.target);
console.log('Target tag:', e.target.tagName);
console.log('Target class:', e.target.className);
console.log('Target text:', e.target.textContent);
// The element with the listener
console.log('Current target:', e.currentTarget);
});
Target vs CurrentTarget
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
// If you click on Item 1:
// e.target = <li class="item">Item 1</li>
// e.currentTarget = <ul id="list">...</ul>
// 'this' also refers to currentTarget
console.log(this === e.currentTarget); // true
});
Finding the Right Element
// Method 1: closest()
const item = e.target.closest('.item');
// Method 2: matches()
if (e.target.matches('.item')) {
// Direct match only
}
// Method 3: Check parent hierarchy
function findAncestor(el, selector) {
while (el && !el.matches(selector)) {
el = el.parentElement;
}
return el;
}
// Method 4: Specific element check
if (e.target.tagName === 'BUTTON') {
// Clicked directly on a button
}
Data Attributes with Delegation
// HTML: <button data-action="delete" data-id="123">X</button>
document.addEventListener('click', (e) => {
const action = e.target.dataset.action;
const id = e.target.dataset.id;
if (action && id) {
actions[action](id);
}
});
const actions = {
delete: (id) => deleteItem(id),
edit: (id) => editItem(id),
view: (id) => viewItem(id)
};
Practical Patterns
Common delegation patterns for real-world applications.
Navigation Menu
const nav = document.querySelector('nav');
nav.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (!link || !nav.contains(link)) return;
e.preventDefault();
const page = link.dataset.page;
navigateTo(page);
// Update active state
nav.querySelector('.active')?.classList.remove('active');
link.classList.add('active');
});
Table Row Selection
const table = document.querySelector('table');
// Select row on click
table.addEventListener('click', (e) => {
const row = e.target.closest('tr');
if (!row || row === table.querySelector('thead tr')) return;
// Toggle selection
row.classList.toggle('selected');
// Get selected rows
const selected = table.querySelectorAll('tr.selected');
console.log(`${selected.length} rows selected`);
});
// Action buttons in rows
table.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
const row = deleteBtn.closest('tr');
const id = row.dataset.id;
deleteRow(id);
return;
}
const editBtn = e.target.closest('.edit-btn');
if (editBtn) {
const row = editBtn.closest('tr');
editRow(row);
}
});
Modal Dialog
const modal = document.getElementById('modal');
modal.addEventListener('click', (e) => {
// Close on backdrop click
if (e.target === modal) {
closeModal();
return;
}
// Handle button clicks
if (e.target.closest('.close-btn')) {
closeModal();
} else if (e.target.closest('.confirm-btn')) {
confirmAction();
closeModal();
}
});
Accordion
const accordion = document.querySelector('.accordion');
accordion.addEventListener('click', (e) => {
const header = e.target.closest('.accordion-header');
if (!header) return;
const item = header.parentElement;
const content = item.querySelector('.accordion-content');
// Toggle this item
const isOpen = content.style.maxHeight;
// Close all others
accordion.querySelectorAll('.accordion-content').forEach(c => {
c.style.maxHeight = null;
});
// Open this one if it was closed
if (!isOpen) {
content.style.maxHeight = content.scrollHeight + 'px';
}
});
Practice Problems
Create a reusable React component implementing DOM Event Delegation. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for DOM Event Delegation using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize DOM Event Delegation for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. Why use closest() instead of checking e.target directly?
2. What does e.currentTarget refer to?
3. Why is event delegation useful for dynamic content?
4. What's the benefit of delegation over direct listeners?
Flashcards
Question
Why use event delegation in DOM?
Click to reveal answer
Answer
Fewer listeners, works with dynamic content, better performance for many elements.
Question
Why use closest() instead of target?
Click to reveal answer
Answer
closest() walks up the DOM tree, handling clicks on child elements. target only matches the exact clicked element.
Question
What is e.currentTarget?
Click to reveal answer
Answer
The element that has the event listener attached, as opposed to e.target which triggered the event.
Question
How to check if a delegated event matches?
Click to reveal answer
Answer
Use e.target.closest(selector) and check if the result exists and is contained within the listener element.
Question
What is DOM Event Delegation?
Click to reveal answer
Answer
DOM Event Delegation is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Event delegation handles events at the parent level
- 2.closest() is essential for finding the right target
- 3.Delegation works with dynamically added elements
- 4.Always verify the target is contained within the parent
- 5.Use data attributes for clean action routing
Interview Tips
- •Implement delegation for a list with dynamic items
- •Explain why closest() is better than checking target directly
- •Show how to handle multiple action types with delegation
- •Discuss delegation patterns for tables and forms
Cheat Sheet
DOM Event Delegation Cheat Sheet
Pattern
parent.addEventListener('click', (e) => {
const target = e.target.closest('.selector');
if (target && parent.contains(target)) {
// Handle event
}
});
Why Delegation?
- Fewer listeners (performance)
- Works with dynamic content
- Easier setup and cleanup
target vs currentTarget
target: Element that triggered eventcurrentTarget: Element with listener
Tips
- Use closest() for target matching
- Always check parent.contains()
- Use data attributes for action routing