Делегирование событий

Один обработчик на родительском элементе вместо отдельных обработчиков на каждом дочернем — использует всплытие событий.

Задача

Нужно обработать клики по элементам списка: кнопкам «удалить», «редактировать» и т.д. Элементы могут добавляться динамически, поэтому вешать обработчик на каждый из них неудобно.

Решение

<ul class="task-list" id="taskList">
  <li class="task" data-id="1">
    Задача 1
    <button class="task__edit" data-action="edit">Ред.</button>
    <button class="task__delete" data-action="delete"></button>
  </li>
  <li class="task" data-id="2">
    Задача 2
    <button class="task__edit" data-action="edit">Ред.</button>
    <button class="task__delete" data-action="delete"></button>
  </li>
</ul>
const list = document.getElementById('taskList');

// Один обработчик на весь список
list.addEventListener('click', (e) => {
  const button = e.target.closest('[data-action]');
  if (!button) return; // клик был не по кнопке

  const action = button.dataset.action;
  const taskEl = button.closest('.task');
  const taskId = taskEl?.dataset.id;

  if (action === 'delete') {
    taskEl.remove();
    console.log('Удалено:', taskId);
  }

  if (action === 'edit') {
    console.log('Редактировать:', taskId);
  }
});

// Добавление новых элементов — обработчик уже работает без изменений
function addTask(id, text) {
  const li = document.createElement('li');
  li.className = 'task';
  li.dataset.id = id;
  li.innerHTML = `
    ${text}
    <button data-action="edit">Ред.</button>
    <button data-action="delete">✕</button>
  `;
  list.appendChild(li);
}

Универсальная утилита делегирования:

function delegate(parent, selector, event, handler) {
  parent.addEventListener(event, (e) => {
    const target = e.target.closest(selector);
    if (target && parent.contains(target)) {
      handler.call(target, e);
    }
  });
}

// Использование
delegate(list, '[data-action="delete"]', 'click', function  {
  this.closest('.task').remove();
});

Ключевые моменты

  • e.target.closest(selector) — находит ближайшего предка (или себя), соответствующего селектору; безопасно при клике на вложенный элемент (иконку внутри кнопки).
  • data-action атрибут — удобный способ хранить тип действия без проверок по классу или тегу.
  • Обработчик на родителе работает и для динамически добавленных дочерних элементов.
  • parent.contains(target) — проверяем, что target находится внутри делегирующего контейнера.

Когда НЕ использовать

  • Для событий, не всплывающих (focus, blur, scroll) — используй capture: true или добавляй обработчик напрямую.
  • Если дерево очень глубокое и клик проходит через много уровней — может быть медленнее прямых обработчиков.

Связанные рецепты / темы