Към съдържанието
wannacode

Събития

Накратко: Събитията казват на JavaScript, че потребителят е кликнал или писал: addEventListener слуша, повечето събития се издигат нагоре по дървото, а делегирането ползва един слушател за много елементи.

Проблем

Новият продукт не се добавя

Каталогът показва два продукта. „Покажи още“ добавя трети. „Добави“ на първите два увеличава кошницата, но „Добави“ на новия продукт не прави нищо. В конзолата няма грешка.

HTML, CSS, JS

Кликни „Добави“ на хляба, после „Покажи още“, после „Добави“ на сиренето. Кошницата остава на 1. Бутонът на сиренето изглежда като другите и има същия текст. Какво му липсва?

Идеята

Събитие и слушател

Когато купувачът кликне, натисне клавиш или пише в поле, браузърът създава събитие: обект, който казва какво е станало и къде. Кодът го чака със слушател:

const button = document.querySelector('#plus');
button.addEventListener('click', (event) => {
console.log(event.type, event.target);
});
  • Първият аргумент е видът на събитието: click, keydown, input, submit.
  • Вторият е функция. Браузърът я вика при всяко събитие и ѝ подава обекта на събитието. Стрелковите функции са от урока за ламбди в курса по JavaScript.
  • Подаваш функцията, а не я викаш: addEventListener('click', save), без скоби. save() би я извикал веднага.

Обектът на събитието има полета:

  • event.target е елементът, в който е станало събитието: бутонът, по който е кликнато.
  • event.currentTarget е елементът, чийто слушател върви сега.
  • event.key е клавишът при keydown: 'Enter', 'Escape', 'a'.

Сега вече е ясно какво липсва в проблема. Слушателите се слагат веднъж, при зареждането, на бутоните, които тогава ги има. Бутонът на сиренето е създаден по-късно и слушател няма.

Пробвай

Плюс и минус

Направи бутоните за броя: „+“ увеличава броя с 1, а „−“ го намалява с 1, но не под 1. Броят се показва в #qty.

HTML, CSS, JS

Идеята

Събитието се издига

Бутонът е в <li>, <li> е в <ul>, а <ul> е в <main>. Клик по бутона е и клик по всеки от тях. Затова събитието не остава в бутона. То тръгва от event.target и се издига нагоре по родителите до document и window. Всеки родител със слушател за click го чува.

document.querySelector('ul').addEventListener('click', (event) => {
console.log(event.target); // бутонът, по който е кликнато
console.log(event.currentTarget); // <ul>: елементът със слушателя
});

Повечето събития се издигат: click, keydown, input, submit. Някои не се издигат: focus и blur. За тях има focusin и focusout, които се издигат.

Виж го

Виж как се издига кликът

Вляво е дървото на каталога. Слушателят е на <ul>, а кликът е по бутона на млякото.

const list = document.querySelector('#products');
list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const item = button.closest('li');
console.log('Добави', item.dataset.id);
});
Стъпка по стъпка, с думи
  1. Слушателят е един и е на списъка <ul>, а не на бутоните. Той чака click.
  2. Купувачът кликва „Добави“ на млякото. Събитието започва от най-вътрешния елемент под курсора: бутона. Това е event.target.
  3. После се издига: от бутона към родителя му <li>, после към <ul>. Всеки родител също чува клика.
  4. На <ul> има слушател и функцията тръгва. event.currentTarget е <ul>, елементът със слушателя. event.target си остава бутонът.
  5. closest('button') започва от самия event.target и върви нагоре. Целта е бутон, затова е намерен веднага.
  6. closest('li') тръгва от бутона нагоре и спира на първия <li>: млякото. data-id казва кой продукт е.
  7. dataset.id е "2". Кошницата знае какво да добави.
  8. Събитието продължава нагоре към <main>, <body>, document и window. Там няма слушатели и нищо друго не става.
  9. Клик по името на хляба също стига до <ul>. Но целта е <h2>: closest('button') не намира бутон над него и връща null, а функцията спира.
  10. Един слушател обслужва всички бутони в списъка, и тези, добавени по-късно: те също са в <ul>. Това се казва делегиране.

Кликът по името на хляба също стига до <ul>. Затова слушателят първо проверява дали е кликнат бутон.

Идеята

Делегиране

Вместо слушател на всеки бутон, слагаш един на общия родител. Той разбира от event.target кой бутон е кликнат. Това се казва делегиране.

list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const id = button.closest('li').dataset.id;
// …добави продукт id
});
  • closest('button') започва от самия елемент и върви нагоре до първия, който пасва на селектора. Ако няма такъв, връща null. Тук пази от клик по името или празното място в <li>.
  • Данните за продукта са на <li> (data-id), а не на бутона. closest('li') стига до тях.

Ползите са две. Новите продукти работят без нищо допълнително: и те са в списъка. А слушателят е един, колкото и да са продуктите.

Пробвай

Поправи каталога

Поправи каталога с един слушател на #products. „Добави“ на всеки продукт, и на добавения с „Покажи още“, увеличава кошницата и пише в конзолата „добавен продукт“ и data-id на продукта. Клик по името на продукта не добавя нищо.

HTML, CSS, JS

Идеята

Действието по подразбиране

Някои събития имат действие, което браузърът прави сам, след слушателите. Клик по връзка отваря адреса ѝ. submit на формуляр изпраща полетата и зарежда новата страница. Това е действието по подразбиране.

Формулярът за търсене тук показва резултата в страницата. Пробвай.

HTML, CSS, JS

Слушателят пише „Търсиш: мляко“, но после браузърът изпраща формуляра. В истинския браузър страницата се зарежда наново с ?q=мляко в адреса и резултатът изчезва. Пясъчникът е една страница: той спира изпращането и пише в конзолата „Формулярът е изпратен“.

Добави event.preventDefault(); в началото на функцията и пусни пак. Редът в конзолата изчезва: действието е спряно, а слушателят си върши работата. Същото важи за връзка, която отваря нещо в страницата, вместо да води към друг адрес.

event.stopPropagation() спира издигането. Рядко е нужно и обикновено е лош знак. Спира и слушатели, за които кодът не знае: делегирането по-горе, затварянето на меню при клик извън него, броенето на кликовете. Ако слушател „чува твърде много“, провери event.target, както прави closest в делегирането.

Пробвай

Намери грешката

Слушателят показва резултата, но формулярът пак се изпраща.

Избери реда с грешката.

Идеята

input или change

Полетата имат две събития за промяна:

  • input идва при всяка промяна на стойността: всяка буква, изтриване, поставяне.
  • change идва, когато купувачът е приключил. При текстово поле това е, когато напусне полето. При отметка, радио бутон и падащо меню е веднага след избора.

За търсене, което филтрира докато пишеш, е input. За падащо меню „Подреди по цена“ е change.

По-точно, за любопитните

Трети аргумент. addEventListener('click', f, { once: true }) слуша само веднъж. removeEventListener('click', f) маха слушателя, но само ако му подадеш същата функция. Стрелкова функция, написана наново, е друга функция.

onclick. button.onclick = f също работи, но е едно поле: втори onclick изтрива първия. addEventListener позволява много слушатели. Атрибутът onclick="…" в HTML смесва кода със страницата, а и се спира от строга политика за сигурност (CSP).

Издигане и улавяне. Пътят на събитието има и обратна посока: първо от window надолу до целта (улавяне, capture), после нагоре (издигане). addEventListener('click', f, { capture: true }) слуша на пътя надолу. Почти винаги трябва издигането.

Клавиатурата. Бутонът <button> получава click и от Enter и интервал. Затова click на бутон работи и с клавиатура. keydown на <div> не я замества (виж урока за достъпност).

event.key срещу event.code. key е знакът ('a' или 'а' според подредбата), а code е мястото на клавиша ('KeyA'). За преки пътища като Escape и Enter е key.

Упражнение

Търсене на живо

Направи търсенето да филтрира списъка, докато купувачът пише: показват се само продуктите, които съдържат текста, без значение от главни и малки букви. Скритият продукт е с hidden. #found показва колко са показани („Намерени: 2“). Escape в полето го изчиства и показва всички. Enter не изпраща формуляра.

HTML, CSS, JS

Тест

Провери се

1. Слушателят е на <ul>, а купувачът кликва бутон в <li>. Какво са event.target и event.currentTarget?
2. Какво не е наред в button.addEventListener('click', save())?
3. Защо делегирането работи и за продукти, добавени след зареждането?
4. Формулярът за търсене има слушател за submit, който показва резултата. Защо е нужно event.preventDefault()?
5. Търсене, което филтрира списъка докато купувачът пише. Кое събитие?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·