Event Delegation
Event delegation is the technique of handling events on a parent element, rather than directly on each child. JavaScript leverages event bubbling — every event that occurs on a child "bubbles up" to its parent.
This technique is very useful for handling events on dynamic elements (added/removed at runtime) and saves memory because only one listener is created, not many.
Why Event Delegation?
Imagine a student list table with 100 rows. If each row has its own click listener, that means 100 active listeners in memory. With event delegation, a single listener on the parent <table> is enough.
Benefits:
- Saves memory — one listener for many children
- Can handle new elements — newly added rows are still handled without re-attaching listeners
- More concise code — no loop needed to attach listeners
Code Example
// Without event delegation — you must attach a listener to each row
const rows = document.querySelectorAll('table tbody tr');
rows.forEach(row => {
row.addEventListener('click', function () {
console.log('Click on:', this.dataset.studentId);
});
});
// With event delegation — a single listener on the table
const table = document.querySelector('table');
table.addEventListener('click', function (event) {
// event.target is the element actually clicked
const row = event.target.closest('tr');
if (row && row.dataset.studentId) {
console.log('Click on StudentID:', row.dataset.studentId);
}
});
// Basic event delegation pattern
document.querySelector('#list-student').addEventListener('click', function (event) {
// Find the matching target element
const item = event.target.closest('.student-item');
if (!item) return; // the click was not on the desired item
// Handle it as needed
const name = item.querySelector('.name').textContent;
console.log('Selected:', name);
});
Code Explanation
event.target — the element that received the event (the deepest element clicked).
this — in an event handler, this refers to the element where the listener was attached (the parent).
const container = document.querySelector('#list-container');
container.addEventListener('click', function (event) {
console.log('event.target:', event.target); // the element clicked directly
console.log('this (currentTarget):', this); // #list-container
console.log('event.currentTarget:', event.currentTarget); // same as this
});
closest() — finds the nearest ancestor matching a selector. Useful for finding the "row" or "card" from a clicked element.
// Handle a click on a delete button inside a student card
document.querySelector('#list').addEventListener('click', function (e) {
const btnDelete = e.target.closest('.btn-delete');
if (!btnDelete) return;
const card = btnDelete.closest('.card-student');
const studentId = card.dataset.studentId;
if (confirm(`Delete data ${studentId}?`)) {
card.remove();
}
});
Real-World Examples
A Dynamic Student Data Table
In an academic portal, the student list table is often populated from a server. New data is added without a page refresh. We need event delegation so that new rows remain clickable.
const studentTable = document.querySelector('#student-table tbody');
// Event delegation — handle all clicks inside the tbody
studentTable.addEventListener('click', function (event) {
const row = event.target.closest('tr[data-studentId]');
if (!row) return;
// Remove the highlight from the previous row
const previous = studentTable.querySelector('tr.active');
if (previous) previous.classList.remove('active');
// Highlight the clicked row
row.classList.add('active');
// Show details in a side panel
const studentId = row.dataset.studentId;
const name = row.querySelector('.col-name').textContent;
showDetails(studentId, name);
});
// Function to add a row from the server
function addRow(student) {
const row = document.createElement('tr');
row.dataset.studentId = student.studentId;
row.innerHTML = `
<td>${student.studentId}</td>
<td class="col-name">${student.name}</td>
<td>${student.major}</td>
<td>${student.year}</td>
`;
studentTable.appendChild(row);
// No need to re-attach the listener!
}
// Simulated data from the server
const newData = [
{ studentId: '2024001', name: 'Mike Johnson', major: 'Computer Science', year: '2024' },
{ studentId: '2024002', name: 'Sarah Chen', major: 'Information Systems', year: '2024' },
];
newData.forEach(student => addRow(student));
Student Filtering
const containerFilter = document.querySelector('#filter-buttons');
containerFilter.addEventListener('click', function (event) {
const btn = event.target.closest('.filter-btn');
if (!btn) return;
// Update the active state
containerFilter.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Filter the table
const major = btn.dataset.major;
const rows = document.querySelectorAll('#student-table tbody tr');
rows.forEach(row => {
if (major === 'all' || row.dataset.major === major) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
Usage Tips
// 1. Always use closest() to find the right parent
event.target.closest('.selector')
// 2. Make sure the selector is specific enough to avoid false positives
// Bad: event.target.closest('div')
// Good: event.target.closest('.card-student')
// 3. Use data attributes to store IDs/data
// <tr data-studentId="2024001">
const studentId = event.target.closest('tr').dataset.studentId;
// 4. Match the event type to the need
// 'click' for the mouse, 'input' for real-time typing
// 'change' for after blur
Conclusion
Event delegation is a must-have pattern for handling events on dynamic elements. Use event.target.closest() to find the right element, store data in data-* attributes, and attach the listener to the parent. This pattern is more efficient and easier to maintain than attaching a listener to every element.