DOM Advanced

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

Освоить паттерн делегирования: один обработчик на родителе вместо множества на дочерних элементах, с использованием event.target и data-* атрибутов.

Продвинутый (Advanced)

Кратко

Делегирование — паттерн: вместо того, чтобы вешать listener на каждый элемент, вешаем ОДИН listener на родителя. При событии проверяем event.target, чтобы понять, какой дочерний элемент был задействован. Это работает благодаря всплытию (bubbling) и автоматически обслуживает динамически добавленные элементы.


Подробно

Проблема без делегирования:

const items = document.querySelectorAll('.item'); items.forEach(item => { item.addEventListener('click', handleClick); // 100 listeners! });

Проблемы:

  • Много listener'ов = память
  • Динамически добавленные элементы не будут работать
  • Нужно снимать listener'и при удалении элементов

Решение — делегирование:

const list = document.querySelector('.list');

list.addEventListener('click', (e) => { const item = e.target.closest('.item'); if (item) { console.log('Клик по:', item.textContent); } });

Почему это работает:

  • Клик на .item → событие всплывает до .list
  • На .list ОДИН listener
  • e.target.closest('.item') находит ближайший элемент с классом
  • Автоматически работает для новых элементов!

data-* атрибуты для идентификации:

<ul class="tabs"> <li data-tab="home">Главная</li> <li data-tab="about">О нас</li> <li data-tab="contacts">Контакты</li> </ul>

document.querySelector('.tabs').addEventListener('click', (e) => { const tab = e.target.closest('[data-tab]'); if (tab) { activateTab(tab.dataset.tab); } });

Когда делегирование НЕ нужно:

  • Мало статических элементов (5-10) — прямые listener'и проще
  • Событие не всплывает (focus, blur)
  • Нужна сложная логика, зависящая от конкретного элемента

Ментальная модель

Делегирование — как ресепшен в офисе. Вместо того чтобы навесить звонок на каждую дверь (100 listener'ов), один звонок на ресепшн. Когда кто-то стучит — ресепшен проверяет, в какую дверь постучали (event.target), и направляет звонок нужному сотруднику.


Примеры

**Самый простой:**

html
<ul id="list">
  <li>Яблоко</li>
  <li>Банан</li>
  <li>Вишня</li>
</ul>

<script>
const list = document.getElementById('list');

list.addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') {
    console.log('Выбрано:', e.target.textContent);
  }
});
</script>

Делегирование: один listener на ul, проверяем tagName.

**Простой:**

html
<div class="todo-list">
  <button data-action="add">Добавить</button>
  <ul>
    <li data-id="1">
      Задача 1
      <button data-action="delete">×</button>
    </li>
  </ul>
</div>

<script>
const todoList = document.querySelector('.todo-list');

todoList.addEventListener('click', (e) => {
  const action = e.target.dataset.action;
  const id = e.target.closest('[data-id]')?.dataset.id;

  if (action === 'delete' && id) {
    console.log(`Удалить задачу ${id}`);
  }
});
</script>

Делегирование с data-* атрибутами: определяем действие и ID через dataset.

**Реальный пример:**

javascript
// Табы с делегированием
document.querySelector('.tabs').addEventListener('click', (e) => {
  const tab = e.target.closest('[data-tab]');
  if (!tab) return;

  // Убираем active у всех
  document.querySelectorAll('.tabs [data-tab]').forEach(t => {
    t.classList.remove('active');
  });

  // Добавляем active выбранному
  tab.classList.add('active');

  // Показываем нужный контент
  const targetId = tab.dataset.tab;
  document.querySelectorAll('.tab-content').forEach(content => {
    content.style.display = content.id === targetId ? 'block' : 'none';
  });
});

Реальные табы: делегирование + closest + data-* для переключения контента.


Частые ошибки

**Неправильно:** Использовать e.target без closest

**Почему:** e.target — тот элемент, куда кликнули. Если внутри li есть span — target будет span, а не li.

**Правильно:** Используйте e.target.closest('.selector'), чтобы найти нужный родительский элемент.

**Неправильно:** Считать, что делегирование ВСЕГДА быстрее

**Почему:** Для 5-10 статических элементов прямые listener'и проще. Делегирование — архитектурный паттерн для динамического контента.

**Правильно:** Делегирование полезно для: динамических списков, табов, модалок. Не для каждого случая.

**Неправильно:** Забывать проверять, что closest вернул элемент

**Почему:** Клик по пустому месту в контейнере: closest вернёт null.

**Правильно:** Всегда проверяйте: if (!element) return;


Важно запомнить

  • Делегирование = один listener на родителе
  • Работает благодаря всплытию (bubbling)
  • e.target.closest('.selector') для поиска элемента
  • Автоматически работает для новых элементов
  • data-* атрибуты для передачи данных
  • Не всегда нужен — оцените пользу

Связь

Назад: Вы понимаете всплытие событий (DA2) — теперь вы используете его для эффективных обработчиков.

Вперёд: Следующий урок (DA4) — как отменять действия браузера по умолчанию: переход по ссылке и отправку формы.

Материалы курса DOM Advanced — PROlab Academy.