Події — частина 2
JavaScript event bubbling — камінь кинутий у воду створює концентричні хвилі
У минулому уроці ми навчилися додавати обробники подій та працювати з об'єктом event. Сьогодні розберемо як саме події рухаються по DOM-дереву, вивчимо потужний патерн event delegation та навчимося працювати з клавіатурними подіями.
Event Bubbling (спливання подій)
Коли ти клікаєш на елемент, подія не залишається тільки на ньому. Вона спливає вгору по DOM-дереву — від елемента до його батька, потім до батька батька, і так до document:
<div class="grandparent">
<div class="parent">
<button class="child">Натисни мене</button>
</div>
</div>
document.querySelector(".grandparent").addEventListener("click", () => {
console.log("Grandparent клік");
});
document.querySelector(".parent").addEventListener("click", () => {
console.log("Parent клік");
});
document.querySelector(".child").addEventListener("click", () => {
console.log("Child клік");
});
// Клік на кнопку виведе ВСІ три:
// "Child клік"
// "Parent клік"
// "Grandparent клік"
Подія "спливає" як бульбашка у воді — від найглибшого елемента до найвищого. Це і є bubbling.
Event Bubbling: подія спливає від button вгору по DOM-дереву
Bubbling — це не баг, а корисна особливість. Саме завдяки спливанню працює event delegation, який ми розглянемо нижче.
stopPropagation — зупинити спливання
Якщо потрібно зупинити спливання:
document.querySelector(".child").addEventListener("click", (event) => {
event.stopPropagation(); // подія не піде далі вгору
console.log("Тільки child");
});
document.querySelector(".parent").addEventListener("click", () => {
console.log("Parent — не спрацює при кліку на child");
});
Використовуй stopPropagation обережно і тільки коли дійсно потрібно. Зупинка спливання може зламати інші обробники, які покладаються на bubbling (наприклад, аналітику або event delegation).
Event Capturing (перехоплення)
Насправді подія проходить три фази:
Три фази події: Capturing, Target, Bubbling
- Capturing (перехоплення) — подія йде зверху вниз:
document->html->body-> ... -> target - Target — подія досягає цільового елемента
- Bubbling (спливання) — подія йде знизу вгору: target -> ... ->
body->html->document
За замовчуванням обробники спрацьовують на фазі bubbling. Щоб слухати на фазі capturing, передай третій аргумент true:
const parent = document.querySelector(".parent");
const child = document.querySelector(".child");
// Фаза capturing (зверху вниз)
parent.addEventListener("click", () => {
console.log("Parent — capturing");
}, true);
// Фаза bubbling (знизу вгору) — за замовчуванням
parent.addEventListener("click", () => {
console.log("Parent — bubbling");
});
child.addEventListener("click", () => {
console.log("Child — target");
});
// Клік на child:
// "Parent — capturing" (фаза 1: зверху вниз)
// "Child — target" (фаза 2: на цільовому елементі)
// "Parent — bubbling" (фаза 3: знизу вгору)
На практиці capturing використовується рідко. Але розуміти три фази важливо для того, щоб знати, чому обробники іноді спрацьовують у несподіваному порядку.
Event Delegation (делегування подій)
Це один з найважливіших патернів у роботі з DOM. Замість того, щоб вішати обробник на кожний елемент списку, ми вішаємо один обробник на батька і визначаємо, на якому саме дочірньому елементі сталася подія:
Проблема: обробник на кожний елемент
// Поганий підхід — обробник на кожен елемент
const items = document.querySelectorAll(".item");
items.forEach((item) => {
item.addEventListener("click", () => {
console.log(item.textContent);
});
});
// Проблеми:
// 1. 100 елементів = 100 обробників у пам'яті
// 2. Нові елементи (додані через JS) не матимуть обробника!
Рішення: event delegation
// Хороший підхід — один обробник на батьківський елемент
const list = document.querySelector(".list");
list.addEventListener("click", (event) => {
// Перевіряємо, чи клікнули саме на елемент списку
if (event.target.classList.contains("item")) {
console.log(event.target.textContent);
}
});
Завдяки bubbling клік на будь-якому .item спливе до .list, де його перехопить наш обробник. event.target покаже, на якому саме елементі клікнули.
Переваги delegation
const list = document.querySelector(".list");
// Один обробник працює для ВСІХ елементів — навіть тих, що додадуться пізніше
list.addEventListener("click", (event) => {
if (event.target.classList.contains("item")) {
event.target.classList.toggle("item--selected");
}
});
// Додаємо новий елемент — обробник автоматично працює для нього!
const newItem = document.createElement("li");
newItem.textContent = "Новий елемент";
newItem.classList.add("item");
list.appendChild(newItem);
Event delegation працює для динамічних списків. Не треба додавати обробники кожному новому елементу. Це також економить пам'ять.
closest() — для вкладених елементів
Якщо всередині .item є інші елементи, event.target може вказувати на дочірній елемент. Метод closest() допоможе:
<ul class="list">
<li class="item">
<span class="item__name">Яблуко</span>
<span class="item__price">25 грн</span>
</li>
</ul>
list.addEventListener("click", (event) => {
// Знаходить найближчий батьківський .item (або сам елемент)
const item = event.target.closest(".item");
if (item) {
console.log("Клік на:", item.querySelector(".item__name").textContent);
}
});
Keyboard Events (клавіатурні події)
| Подія | Коли спрацьовує |
|---|---|
keydown | Клавішу натиснули (спрацьовує першою) |
keyup | Клавішу відпустили |
document.addEventListener("keydown", (event) => {
console.log("Клавіша:", event.key); // "a", "Enter", "Escape", "ArrowUp"
console.log("Код:", event.code); // "KeyA", "Enter", "Escape", "ArrowUp"
});
event.key vs event.code
| Властивість | Що показує | Приклад |
|---|---|---|
event.key | Символ, який вводиться | "a", "A", "Enter" |
event.code | Фізичну клавішу | "KeyA", "KeyA", "Enter" |
// event.key залежить від розкладки клавіатури
// event.code — завжди однаковий для фізичної клавіші
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
console.log("Натиснули Escape — закриваємо модальне вікно");
}
if (event.key === "Enter") {
console.log("Натиснули Enter — підтверджуємо дію");
}
});
Клавіші-модифікатори
document.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.key === "s") {
console.log("Ctrl+S — зберігаємо!");
}
if (event.shiftKey && event.key === "Enter") {
console.log("Shift+Enter — новий рядок");
}
});
| Властивість | Клавіша |
|---|---|
event.ctrlKey | Ctrl (або Cmd на Mac) |
event.shiftKey | Shift |
event.altKey | Alt (або Option на Mac) |
event.metaKey | Cmd на Mac / Win на Windows |
preventDefault — скасування дії за замовчуванням
Деякі події мають дію за замовчуванням від браузера. preventDefault() скасовує її:
// Клік на посилання — за замовчуванням браузер переходить за href
const link = document.querySelector("a");
link.addEventListener("click", (event) => {
event.preventDefault(); // не переходимо за посиланням
console.log("Клік на посилання, але переходу не буде");
});
// Submit форми — за замовчуванням перезавантажує сторінку
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault(); // не перезавантажуємо сторінку
console.log("Обробляємо форму через JavaScript");
});
preventDefault() дуже часто використовується з формами. У сучасних додатках ми обробляємо форми через JavaScript (відправляємо дані через fetch), а не через стандартну перезавантаження сторінки.
Практика: делегований список з клавіатурою
<div class="todo-app">
<input type="text" class="todo-input" placeholder="Нове завдання..." />
<ul class="todo-list"></ul>
</div>
const input = document.querySelector(".todo-input");
const list = document.querySelector(".todo-list");
// Додавання завдання по Enter
input.addEventListener("keydown", (event) => {
if (event.key === "Enter" && input.value.trim() !== "") {
const li = document.createElement("li");
li.classList.add("todo-item");
li.textContent = input.value.trim();
list.appendChild(li);
input.value = "";
}
});
// Delegation — видалення по кліку (працює і для нових елементів)
list.addEventListener("click", (event) => {
if (event.target.classList.contains("todo-item")) {
event.target.classList.toggle("todo-item--done");
}
});
// Закриття по Escape
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
input.value = "";
input.blur(); // знімаємо фокус з поля
}
});
Підсумок
- Bubbling — подія спливає від target вгору по DOM-дереву
stopPropagation()— зупиняє спливання (використовуй обережно)- Capturing — зворотна фаза (зверху вниз), вмикається третім аргументом
true - Event delegation — один обробник на батьку замість багатьох на дочірніх
closest()— знаходить найближчий батьківський елемент за селекторомkeydown / keyup— клавіатурні події,event.keyдля символуpreventDefault()— скасовує стандартну дію браузера (перехід, submit)
Що далі?
У наступному уроці вивчимо форми та localStorage — як отримувати дані з форм та зберігати інформацію в браузері.
Корисні посилання:
- javascript.info: Спливання та перехоплення — bubbling та capturing (українською)
- javascript.info: Делегування подій — патерн delegation (українською)
- MDN: KeyboardEvent.key — всі значення key
- keycode.info — інтерактивний інструмент для визначення event.key та event.code