Пошук уроків, статей та іншого контенту
Застосовуватимете делегування подій для ефективної роботи з динамічно створеними елементами.
Делегування подій — це техніка, за якої обробник події додають не кожному дочірньому елементу, а спільному батьківському елементу.
Наприклад, замість додавання обробника кожній кнопці:
const buttons = document.querySelectorAll('.delete-button');
buttons.forEach((button) => {
button.addEventListener('click', handleDelete);
});можна додати один обробник контейнеру:
const list = document.querySelector('.list');
list.addEventListener('click', handleDelete);Це працює завдяки спливанню подій: після виникнення події на дочірньому елементі вона передається його батьківським елементам.
Делегування особливо корисне, коли:
елементів багато;
дочірні елементи створюються динамічно;
дочірні елементи можуть видалятися;
потрібно зменшити кількість обробників подій.
Коли користувач натискає на елемент, подія проходить кілька етапів:
Від документа до цільового елемента — фаза захоплення.
На самому цільовому елементі — фаза цілі.
Від цільового елемента назад до документа — фаза спливання.
За замовчуванням addEventListener слухає події під час фази спливання.
Розглянемо HTML:
<div class="card">
<button class="button">Натиснути</button>
</div>Якщо натиснути кнопку, обробник події на div.card також може спрацювати:
const card = document.querySelector('.card');
card.addEventListener('click', () => {
console.log('Подія дійшла до картки');
});Подія click виникла на кнопці, але потім спливла до картки.
target і currentTargetУ делегуванні важливо розрізняти дві властивості об'єкта події.
event.targettarget — це елемент, на якому подія виникла безпосередньо.
event.currentTargetcurrentTarget — це елемент, на якому зараз виконується обробник.
<div class="container">
<button class="button">
<span>Зберегти</span>
</button>
</div>const container = document.querySelector('.container');
container.addEventListener('click', (event) => {
console.log('target:', event.target);
console.log('currentTarget:', event.currentTarget);
});Якщо користувач натисне на span, значення будуть такими:
event.target — span;
event.currentTarget — div.container.
У делегуванні обробник зазвичай перевіряє event.target або шукає потрібного предка відносно event.target.
Припустімо, є список завдань:
<ul class="tasks">
<li class="task">
Вивчити делегування
<button type="button" data-action="complete">Готово</button>
<button type="button" data-action="remove">Видалити</button>
</li>
</ul>Замість обробника на кожну кнопку можна слухати подію на списку:
const tasks = document.querySelector('.tasks');
tasks.addEventListener('click', (event) => {
if (event.target.dataset.action === 'complete') {
event.target.closest('.task').classList.toggle('is-completed');
}
if (event.target.dataset.action === 'remove') {
event.target.closest('.task').remove();
}
});У цьому прикладі:
tasks — спільний контейнер;
event.target — натиснута кнопка;
dataset.action — значення атрибута data-action;
closest('.task') — найближчий предок із класом .task;
remove() — видалення елемента з DOM.
closestМетод closest(selector) шукає найближчий елемент, який відповідає CSS-селектору. Пошук починається із самого елемента, на якому викликано метод, і рухається вгору до предків.
<li class="task">
<button class="task-button">
<span class="button-icon">×</span>
Видалити
</button>
</li>Якщо користувач натисне на span, перевірка лише через event.target буде ненадійною:
if (event.target.classList.contains('task-button')) {
// Цей код не спрацює, якщо натиснуто на span
}Натомість closest знайде кнопку незалежно від того, на яку її внутрішню частину натиснули:
const button = event.target.closest('.task-button');
if (button) {
const task = button.closest('.task');
task.remove();
}matchesМетод matches(selector) перевіряє, чи відповідає елемент заданому CSS-селектору.
const list = document.querySelector('.list');
list.addEventListener('click', (event) => {
if (event.target.matches('.remove-button')) {
const item = event.target.closest('.list-item');
item.remove();
}
});Однак для кнопок із вкладеними елементами частіше краще використовувати closest:
list.addEventListener('click', (event) => {
const removeButton = event.target.closest('.remove-button');
if (!removeButton) {
return;
}
const item = removeButton.closest('.list-item');
item.remove();
});Такий варіант працює і при натисканні на саму кнопку, і при натисканні на її дочірній елемент.
Одна з головних переваг делегування — обробник працює і для елементів, які з'явилися після завантаження сторінки.
Повний приклад списку завдань:
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Делегування подій</title>
<style>
.task {
margin-bottom: 8px;
}
.task.is-completed {
text-decoration: line-through;
opacity: 0.6;
}
</style>
</head>
<body>
<form class="task-form">
<input
class="task-input"
type="text"
placeholder="Нове завдання"
required
>
<button type="submit">Додати</button>
</form>
<ul class="task-list"></ul>
<script>
const form = document.querySelector('.task-form');
const input = document.querySelector('.task-input');
const taskList = document.querySelector('.task-list');
form.addEventListener('submit', (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) {
return;
}
taskList.insertAdjacentHTML(
'beforeend',
`
<li class="task">
<span class="task-text">${text}</span>
<button type="button" data-action="complete">
Готово
</button>
<button type="button" data-action="remove">
Видалити
</button>
</li>
`
);
form.reset();
input.focus();
});
taskList.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
if (!button || !taskList.contains(button)) {
return;
}
const task = button.closest('.task');
const action = button.dataset.action;
if (action === 'complete') {
task.classList.toggle('is-completed');
}
if (action === 'remove') {
task.remove();
}
});
</script>
</body>
</html>Обробник для taskList встановлюється лише один раз. Він обробляє натискання на всі поточні та майбутні кнопки.
У реальному застосунку значення, введене користувачем, потрібно безпечно додавати через
textContent, а не вставляти безпосередньо в HTML. У прикладі вищеinsertAdjacentHTMLвикористано для стислості демонстрації механізму делегування.
Безпечніший варіант створення завдання:
const task = document.createElement('li');
task.className = 'task';
const text = document.createElement('span');
text.className = 'task-text';
text.textContent = input.value.trim();
const completeButton = document.createElement('button');
completeButton.type = 'button';
completeButton.dataset.action = 'complete';
completeButton.textContent = 'Готово';
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.dataset.action = 'remove';
removeButton.textContent = 'Видалити';
task.append(text, completeButton, removeButton);
taskList.append(task);data-* атрибутівАтрибути data-* зручно використовувати для зберігання інформації про дію або ідентифікатор елемента.
<ul class="user-list">
<li class="user" data-user-id="42">
<span>Олена</span>
<button type="button" data-action="edit">Редагувати</button>
<button type="button" data-action="remove">Видалити</button>
</li>
</ul>const userList = document.querySelector('.user-list');
userList.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
if (!button) {
return;
}
const user = button.closest('.user');
const userId = user.dataset.userId;
const action = button.dataset.action;
if (action === 'edit') {
console.log(`Редагування користувача ${userId}`);
}
if (action === 'remove') {
console.log(`Видалення користувача ${userId}`);
}
});Атрибут:
data-user-id="42"доступний у JavaScript як:
element.dataset.userIdАтрибут:
data-action="remove"доступний як:
element.dataset.actionМетод closest може знайти елемент за межами контейнера, якщо подія виникла у вкладеній структурі або DOM змінюється під час виконання коду.
Тому іноді корисно перевірити, що знайдений елемент справді належить потрібному контейнеру:
const container = document.querySelector('.container');
container.addEventListener('click', (event) => {
const button = event.target.closest('.button');
if (!button || !container.contains(button)) {
return;
}
console.log('Натиснуто кнопку всередині контейнера');
});У більшості простих випадків перевірка container.contains(button) не потрібна, але вона робить обробник стійкішим до складних змін DOM.
Делегування можна застосовувати не лише до кнопок. Наприклад, контейнер може обробляти натискання на всі посилання:
const navigation = document.querySelector('.navigation');
navigation.addEventListener('click', (event) => {
const link = event.target.closest('a');
if (!link || !navigation.contains(link)) {
return;
}
if (link.hasAttribute('data-confirm')) {
const shouldNavigate = window.confirm('Перейти за посиланням?');
if (!shouldNavigate) {
event.preventDefault();
}
}
});event.preventDefault() скасовує стандартну дію браузера. У цьому випадку перехід за посиланням не відбудеться, якщо користувач натиснув «Скасувати».
Події форми також можна делегувати. Наприклад, один обробник change може працювати з усіма прапорцями всередині форми:
<form class="settings-form">
<label>
<input type="checkbox" name="emailNotifications">
Email-сповіщення
</label>
<label>
<input type="checkbox" name="pushNotifications">
Push-сповіщення
</label>
</form>const settingsForm = document.querySelector('.settings-form');
settingsForm.addEventListener('change', (event) => {
if (!event.target.matches('input[type="checkbox"]')) {
return;
}
console.log(
`${event.target.name}: ${event.target.checked}`
);
});Так можна обробляти і прапорці, які були додані до форми пізніше.
Не всі події спливають однаково. Наприклад:
click спливає;
input спливає;
change зазвичай можна делегувати;
focus і blur не спливають;
focusin і focusout спливають.
Тому для делегування фокусу використовують focusin і focusout:
const form = document.querySelector('.form');
form.addEventListener('focusin', (event) => {
if (event.target.matches('input')) {
event.target.classList.add('is-focused');
}
});
form.addEventListener('focusout', (event) => {
if (event.target.matches('input')) {
event.target.classList.remove('is-focused');
}
});Метод event.stopPropagation() зупиняє подальше поширення події.
const list = document.querySelector('.list');
list.addEventListener('click', () => {
console.log('Обробник списку');
});
const button = document.querySelector('.button');
button.addEventListener('click', (event) => {
event.stopPropagation();
console.log('Обробник кнопки');
});Після натискання на кнопку обробник кнопки виконається, але обробник списку — ні.
Не варто використовувати stopPropagation() без потреби. Він може приховати логіку інших компонентів, які також слухають цю подію.
Метод stopImmediatePropagation() додатково зупиняє виконання інших обробників події на тому самому елементі. Це ще сильніше втручання, тому його слід застосовувати обережно.
stopPropagationДелегування не працюватиме, якщо дочірній елемент або його предок зупинить спливання події:
const button = document.querySelector('.button');
button.addEventListener('click', (event) => {
event.stopPropagation();
});У такому разі подія не дійде до контейнера, на якому розташований делегований обробник.
Якщо ви контролюєте код компонента, переконайтеся, що stopPropagation() використовується лише тоді, коли це справді необхідно.
У складних компонентах події можуть проходити через Shadow DOM із властивістю composed. Якщо подія перетинає межу Shadow DOM, звичайний event.target може бути зовнішнім елементом, а не внутрішнім елементом компонента.
Для аналізу повного шляху події існує метод:
event.composedPath()У звичайних застосунках без Shadow DOM достатньо target, currentTarget і closest.
Делегування не завжди є найкращим рішенням. Окремі обробники можуть бути зрозумілішими, якщо:
елементів дуже мало;
кожен елемент має повністю різну поведінку;
подія не спливає;
між елементом і контейнером є компонент, який зупиняє спливання;
обробник має бути прив'язаний до конкретного елемента і не повинен реагувати на майбутні елементи.
Наприклад, для однієї кнопки простіше написати:
const button = document.querySelector('.save-button');
button.addEventListener('click', saveData);Делегування варто застосовувати тоді, коли воно спрощує код або вирішує проблему динамічних елементів.
Для кнопок усередині контейнера часто використовується такий шаблон:
const container = document.querySelector('.container');
container.addEventListener('click', (event) => {
const control = event.target.closest('[data-action]');
if (!control || !container.contains(control)) {
return;
}
const action = control.dataset.action;
const item = control.closest('[data-item]');
if (!item) {
return;
}
switch (action) {
case 'open':
console.log('Відкриття елемента');
break;
case 'remove':
item.remove();
break;
default:
console.warn(`Невідома дія: ${action}`);
}
});Переваги такого підходу:
обробник встановлюється один раз;
підтримуються вкладені елементи кнопки;
дія визначається через data-action;
код легко розширювати новими діями;
динамічно додані елементи працюють автоматично.
function addTask(task) {
task.addEventListener('click', handleTaskClick);
}Якщо елемент створюється в різних місцях програми, легко забути додати обробник. Делегування усуває цю проблему:
taskList.addEventListener('click', handleTaskClick);event.targetif (event.target.classList.contains('remove-button')) {
// Логіка видалення
}Це не спрацює, якщо всередині кнопки є span, svg або інший елемент.
Краще:
const button = event.target.closest('.remove-button');
if (!button) {
return;
}nullМетод closest може не знайти потрібний елемент і повернути null.
Небезпечний код:
const item = event.target.closest('.item');
item.remove();Безпечний код:
const item = event.target.closest('.item');
if (!item) {
return;
}
item.remove();event.currentTarget замість event.targetУ делегованому обробнику currentTarget — це контейнер:
container.addEventListener('click', (event) => {
console.log(event.currentTarget); // container
console.log(event.target); // фактичний елемент натискання
});Якщо потрібно визначити натиснуту кнопку, використовуйте event.target або event.target.closest(...).
document без потребиТехнічно можна слухати подію на всьому документі:
document.addEventListener('click', handleClick);Але це ускладнює контроль над областю дії та може спричинити конфлікти. Краще вибирати найближчий спільний контейнер:
const list = document.querySelector('.list');
list.addEventListener('click', handleClick);stopPropagationЯкщо делегований обробник не спрацьовує, перевірте, чи не викликається десь stopPropagation().
Створіть список товарів із такими можливостями:
Кожен товар має кнопки «Додати до кошика» і «Видалити».
Нові товари додаються після натискання окремої кнопки.
Усі дії обробляються одним обробником на контейнері.
Назва дії зберігається в data-action.
Для визначення товару використовується closest.
Орієнтовний HTML:
<button class="add-product" type="button">
Додати товар
</button>
<ul class="product-list">
<li class="product" data-product-id="1">
<span class="product-name">Клавіатура</span>
<button type="button" data-action="cart">До кошика</button>
<button type="button" data-action="remove">Видалити</button>
</li>
</ul>Під час реалізації перевірте, що делегований обробник працює і для товарів, створених після завантаження сторінки.
Делегування подій — це встановлення одного обробника на спільному батьківському елементі.
Воно працює завдяки спливанню подій.
event.target — елемент, на якому виникла подія.
event.currentTarget — елемент із поточним обробником.
closest допомагає знайти потрібний батьківський елемент.
matches перевіряє відповідність елемента CSS-селектору.
data-* атрибути зручно використовувати для зберігання дій та ідентифікаторів.
Делегування автоматично працює з динамічно створеними елементами.
Для подій фокусу використовують focusin і focusout, оскільки focus і blur не спливають.
stopPropagation() може перешкодити делегованому обробнику отримати подію.
Делегування потрібно застосовувати там, де воно справді спрощує код і покращує роботу з DOM.