Event delegation
TL;DR
Event delegation é o padrão de colocar um único listener no container (em vez de um por item filho) e usar
event.target.closest()para identificar qual item foi clicado. Funciona porque eventos borbulham. Vantagens: menos memória, suporte automático a itens adicionados dinamicamente, código mais simples. A única armadilha: elementos que não borbulham (focus,blur) — usefocusin/focusoutno lugar.
O problema com um listener por item
// ❌ Um listener por item — N listeners para N items
const items = document.querySelectorAll('.todo-item');
items.forEach(item => {
item.addEventListener('click', (event) => {
toggleTodo(item.dataset.id);
});
});
// Problemas:
// 1. Memória: 1000 itens = 1000 listeners
// 2. Itens adicionados dinamicamente não têm listener
// 3. Ao remover itens do DOM, os listeners precisam ser removidos manualmenteEvent delegation — um listener no container
// ✅ Um único listener no container
const list = document.querySelector('.todo-list');
list.addEventListener('click', (event) => {
// event.target = o elemento que foi CLICADO
// closest() sobe até encontrar o item — independente de qual filho foi clicado
const item = event.target.closest('.todo-item');
if (!item) return; // clique fora de qualquer item
toggleTodo(item.dataset.id);
});Se a estrutura do item for:
<li class="todo-item" data-id="42">
<input type="checkbox">
<span class="todo-item__label">Fazer compras</span>
<button class="btn btn--delete">✕</button>
</li>Clicar no <span>, no <input> ou no <button> — em todos os casos, event.target.closest('.todo-item') retorna o <li> correto.
Múltiplas ações no mesmo container
const list = document.querySelector('.task-list');
list.addEventListener('click', (event) => {
// Verificar qual botão foi clicado usando closest() e matches()
const deleteBtn = event.target.closest('.btn--delete');
const editBtn = event.target.closest('.btn--edit');
const checkbox = event.target.closest('input[type="checkbox"]');
if (deleteBtn) {
const item = deleteBtn.closest('.task-item');
deleteTask(item.dataset.id);
return;
}
if (editBtn) {
const item = editBtn.closest('.task-item');
openEditor(item.dataset.id);
return;
}
if (checkbox) {
const item = checkbox.closest('.task-item');
toggleTask(item.dataset.id);
return;
}
});Pattern alternativo com data-action
Mais declarativo — a responsabilidade de “o que fazer” fica no HTML:
<li class="task-item" data-id="42">
<input type="checkbox" data-action="toggle">
<span class="task-item__label">Tarefa</span>
<button data-action="edit">Editar</button>
<button data-action="delete">Deletar</button>
</li>const handlers = {
toggle: (el) => toggleTask(el.closest('.task-item').dataset.id),
edit: (el) => openEditor(el.closest('.task-item').dataset.id),
delete: (el) => deleteTask(el.closest('.task-item').dataset.id),
};
list.addEventListener('click', (event) => {
const trigger = event.target.closest('[data-action]');
if (!trigger) return;
const action = trigger.dataset.action;
const handler = handlers[action];
if (handler) handler(trigger);
});Suporte automático a itens dinâmicos
O maior benefício de delegation: itens adicionados depois do listener já são cobertos.
// Lista com delegation já registrado
const list = document.querySelector('.todo-list');
list.addEventListener('click', handleListClick);
// Adicionar item depois — automaticamente ouve os eventos
function addTodoItem(todo) {
const li = document.createElement('li');
li.className = 'todo-item';
li.dataset.id = todo.id;
li.innerHTML = `
<span>${escapeHtml(todo.label)}</span>
<button data-action="delete">✕</button>
`;
list.appendChild(li); // o listener do container já cobre este novo item
}Armadilhas do event delegation
1. Eventos que não borbulham
// ❌ focus e blur não borbulham — delegation não funciona
list.addEventListener('focus', handleFocus); // não pega os inputs filhos
// ✅ Use focusin/focusout (borbulham)
list.addEventListener('focusin', handleFocus);
list.addEventListener('focusout', handleBlur);
// ✅ Ou use capture: true (o evento desce e você o pega)
list.addEventListener('focus', handleFocus, { capture: true });2. stopPropagation em filhos quebra delegation
// Se algum handler filho para a propagação, o container não recebe o evento
item.addEventListener('click', (event) => {
event.stopPropagation(); // ❌ mata o delegation do container!
doSomething();
});
// ✅ Evite stopPropagation quando usando delegation
// Prefira event.preventDefault() se precisar bloquear comportamento do browser3. Múltiplas funções closest() — performatico, mas pode ser repetitivo
// Se você chama closest() muitas vezes com o mesmo seletor, pode ser custoso em listas grandes
// Solução: chamar uma vez e reutilizar
const item = event.target.closest('.item');
if (!item) return;
const id = item.dataset.id;
const label = item.querySelector('.item__label'); // dentro do item encontradoDelegation em tabelas — armadilha de CSS
Tabelas com border-collapse: collapse têm um comportamento peculiar: o <td> recebe o evento de clique, mas o <tr> pode não ser o ancestral imediato dependendo de como o DOM da tabela está estruturado.
<table>
<tbody>
<tr data-row-id="1" class="data-row">
<td>João</td>
<td>30</td>
<td><button data-action="edit">Editar</button></td>
</tr>
</tbody>
</table>const table = document.querySelector('table');
table.addEventListener('click', (event) => {
// ✅ closest() resolve a ambiguidade
const row = event.target.closest('.data-row');
if (!row) return;
const action = event.target.closest('[data-action]')?.dataset.action;
if (action === 'edit') editRow(row.dataset.rowId);
else selectRow(row.dataset.rowId);
});Implementar um mini event emitter com delegation
// Sistema de eventos declarativo baseado em delegation
function createEventSystem(root) {
const handlers = new Map(); // action → [handlers]
root.addEventListener('click', (event) => {
const trigger = event.target.closest('[data-action]');
if (!trigger) return;
const action = trigger.dataset.action;
const list = handlers.get(action) || [];
list.forEach(fn => fn(trigger, event));
});
return {
on(action, handler) {
if (!handlers.has(action)) handlers.set(action, []);
handlers.get(action).push(handler);
},
off(action, handler) {
const list = handlers.get(action) || [];
handlers.set(action, list.filter(fn => fn !== handler));
}
};
}
const app = createEventSystem(document.querySelector('#app'));
app.on('save', (el) => saveItem(el.closest('[data-id]').dataset.id));
app.on('delete', (el) => deleteItem(el.closest('[data-id]').dataset.id));Para fixar
- Explique event delegation em uma frase. Por que funciona?
- Uma lista tem 500 itens com um botão de deletar cada. Compare o uso de memória entre: 500 listeners individuais vs delegation no container.
- Você adiciona 10 novos itens à lista após o DOMContentLoaded. Com delegation, eles precisam de listeners novos?
- Por que
delegationnão funciona comfocusdiretamente? Como resolver?- O que acontece com o delegation no container se um handler filho chama
stopPropagation()?
Veja também
- 03 — Formulário e foco — anterior
- 05 — Custom events — próxima
- DOM 02 — Seleção — closest() como base do delegation