Делегирование событий JavaScript: обработчик для динамического списка

Делегирование событий JavaScript: обработчик для динамического списка
Содержание статьи

При делегировании событий обработчик ставят на общий контейнер. Всплывающий click позволяет ему обрабатывать нажатия на подходящие дочерние элементы, включая добавленные позже. Но одного поиска кнопки недостаточно: нужно убедиться, что она относится именно к нужному списку.

В упражнении Mentoring.digital сделаем список задач с удалением. Все операции выполняются только в локальной странице, без запросов к серверу.

Соберите небольшой рабочий пример

Сохраните разметку как tasks.html и откройте её в браузере. Обработчик установлен на ul один раз. Вложенный span нужен специально: пользователь может нажать на текст внутри кнопки, и целью события станет этот span.

<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<title>Учебный список задач</title>
<button id="add" type="button">Добавить задачу</button>
<ul id="tasks" data-task-list>
  <li data-task>
    Первая задача
    <button type="button" data-action="remove">
      <span>Удалить</span>
    </button>
  </li>
</ul>
<script>
const list = document.querySelector("#tasks");
let number = 1;

list.addEventListener("click", (event) => {
  if (!(event.target instanceof Element)) return;
  const button = event.target.closest(
    'button[data-action="remove"]'
  );
  if (!button || !list.contains(button)) return;
  if (button.closest("[data-task-list]") !== list) return;
  const task = button.closest("li[data-task]");
  if (!task || task.parentElement !== list) return;
  task.remove();
});

document.querySelector("#add").addEventListener("click", () => {
  const task = document.createElement("li");
  task.dataset.task = "";
  task.append(`Задача ${++number} `);
  const button = document.createElement("button");
  button.type = "button";
  button.dataset.action = "remove";
  button.textContent = "Удалить";
  task.append(button);
  list.append(task);
});
</script>
</html>

Различайте target и currentTarget

event.target указывает на исходную цель события, а currentTarget внутри этого слушателя будет ul. Проверка target.tagName === 'BUTTON' пропустила бы нажатие на span. Метод closest поднимается от элемента к его предкам и находит подходящую кнопку.

Проверка instanceof Element нужна перед вызовом closest. Она не заменяет остальные условия. contains удерживает найденную кнопку внутри контейнера, а проверка ближайшего data-task-list защищает от обработки кнопок вложенного независимого списка. Последняя проверка разрешает удалять только непосредственную задачу нашего ul.

Почему новые кнопки тоже работают

Код добавления не устанавливает отдельный click на каждую кнопку. Новый элемент становится потомком уже существующего ul, и его событие проходит через тот же контейнер. Это удобно, когда строки добавляются, удаляются или перерисовываются.

Если заменить сам ul новым узлом, старый обработчик останется на старом узле. Делегирование не переносит слушатель автоматически. Поэтому выбирайте стабильный контейнер и следите за тем, что именно заменяет рендеринг вашего интерфейса.

Проверьте границы поведения

  1. Нажмите на надпись «Удалить» в первой кнопке, затем на её свободный край.
  2. Добавьте две задачи и удалите только одну из них.
  3. Нажмите на текст задачи вне кнопки. Строка должна остаться.
  4. Добавьте вложенный ul с data-task-list и своей кнопкой. Внешний обработчик не должен удалять его строки.

Не все события всплывают одинаково. Например, для фокуса применяют focusin либо обработку на фазе перехвата, а не механически заменяют click на focus. Вызов stopPropagation в дочернем обработчике может остановить путь до контейнера. В Shadow DOM дополнительно учитывайте переназначение цели события.

Работу событий можно включить в практический курс JavaScript. Для разбора сложного компонента покажите наставнику по JavaScript минимальную разметку и точную последовательность кликов.

Похожие статьи по данной теме