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

  1. What is Event Delegation?
  2. How Delegation Works
  3. Delegation Patterns
  4. Dynamic Content Handling
  5. 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

PatternWhen to UseExample
Tag matchingSimple lists of same elementevent.target.tagName === "LI"
Class matchingElements with specific classesevent.target.classList.contains("btn")
closest()Nested content, complex structureevent.target.closest(".card")
matches()Complex CSS selectorsevent.target.matches("button.primary")
Data attributesAction-based handlingevent.target.dataset.action

💡 Key Takeaways

  1. Single Handler: Attach one listener to a parent instead of many to children
  2. Use closest(): Find the meaningful element when content is nested
  3. Check Targets: Always verify the clicked element before acting
  4. Dynamic Content: Delegation automatically handles new elements
  5. 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.

.4 Event Delegation - JavaScript Tutorial | DeepML