Skillversitas

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.