События: реакция на клики, ввод и клавиши
Зачем:Страница оживает, когда реагирует на посетителя. После урока вы вешаете обработчики на клики, ввод и клавиши, понимаете, как событие всплывает по дереву и зачем делегирование, и не ломаете управление с клавиатуры.
25 минут
Озвучено синтезом речи (Qwen3-TTS), голос не принадлежит человеку.
Событие — сообщение браузера о действии посетителя: клике, вводе, нажатии клавиши. Скрипт подписывается на него через addEventListener и реагирует.
Страница, которая слушает
Браузер непрерывно замечает, что делает посетитель: нажал, провёл, напечатал букву, переключил вкладку, повернул телефон. Каждое такое замечание браузер оформляет как событие и сообщает странице. Страница ничего не делает, пока не «подписана» на событие, а подписать её должен скрипт.
const button = document.querySelector('#menu-button');
button.addEventListener('click', (event) => {
console.log('Нажали, тип события:', event.type);
}); Метод addEventListener принимает два аргумента: тип события и обработчик, функцию, которую браузер вызовет, когда событие случится. Ей он передаёт объект события: у него есть type (какое событие), target (где оно возникло), для клавиатуры key (какая клавиша), и многое другое. Снять подписку можно через removeEventListener.
Старых способов два, и оба хуже. Атрибут в разметке (<button onclick="...">) смешивает разметку с кодом и позволяет повесить лишь один обработчик. Свойство (button.onclick = ...) тоже даёт один обработчик на событие: следующий затрёт предыдущий. addEventListener позволяет подписывать на одно событие сколько угодно функций.
События, которые нужны сайту
Событий десятки, но почти всё на сайте строится на пяти:
click: нажатие мышью или пальцем, а у кнопки ещё и клавиши Enter и пробел;input: значение поля меняется, пока человек печатает (аchangeсрабатывает, когда он закончил, например отпустил поле или переключил флажок);submit: отправка формы, по кнопке или по Enter;keydown: нажата клавиша,event.keyназывает её ('Escape','Enter','a');DOMContentLoaded: страница разобрана. В нашем редакторе модульные скрипты и так выполняются после этого момента, поэтому слушать это событие не нужно.
Отмена действия браузера
У многих событий есть действие по умолчанию: ссылка ведёт по адресу, форма отправляется и страница перезагружается, Tab переводит фокус. Когда нужно сделать по-своему, действие отменяют вызовом event.preventDefault().
form.addEventListener('submit', (event) => {
event.preventDefault(); // страница не перезагрузится
// дальше отправляем данные сами: это урок про fetch
}); Без этой строки Enter в поле поиска отправит форму и страница перезагрузится с пустым результатом: самая частая ошибка начинающего.
Проверьте себя: что будет, если в обработчике submit забыть preventDefault?
Браузер выполнит действие по умолчанию: отправит форму и перезагрузит страницу, и всё, что скрипт успел сделать, пропадёт.
Всплытие и делегирование
Событие не остаётся в одном месте. Когда вы нажимаете на ссылку внутри карточки, браузер сначала передаёт событие самой ссылке, а потом всплывает вверх по предкам: карточка, контейнер списка, body, document. Каждый из них может иметь свой обработчик и получить то же событие. Поэтому у объекта события два адреса:
event.target: где событие возникло (самый глубокий элемент, по которому нажали);event.currentTarget: на ком висит сейчас сработавший обработчик.
Это даёт приём, которым пользуются все: делегирование. Вместо обработчика на каждой из ста карточек вешают один на контейнер и смотрят в event.target, что именно нажали:
cards.addEventListener('click', (event) => {
const card = event.target.closest('.card'); // ближайшая карточка вверх от места нажатия
if (!card) return; // нажали мимо
card.classList.toggle('selected');
}); Метод closest ищет подходящий элемент от места нажатия вверх по дереву. Выгод две. Слушателей меньше, а главное, обработчик работает и для карточек, которые скрипт добавит позже: события от них тоже всплывут до контейнера. Поэтому, если у вас список, который меняется, вешайте обработчик на родителя.
Остановить всплытие можно через event.stopPropagation(), но делать это стоит редко: оно ломает чужие обработчики, которые вы могли не учесть.
Посмотрите, как событие идёт по дереву. Выберите, куда нажимать, на каких узлах стоят обработчики, и попробуйте остановить всплытие:
a: событие проходит сквозь, обработчика нетarticle.card: событие проходит сквозь, обработчика нетdiv.cards: обработчик сработал: target = a, currentTarget = div.cardsbody: событие проходит сквозь, обработчика нетdocument: событие проходит сквозь, обработчика нет
Что ещё случится. Браузер перейдёт по ссылке: действие по умолчанию никто не отменил.
И второй вопрос схемы: сколько обработчиков нужно, когда карточек становится больше и когда одну из них добавил скрипт:
Нажмите на карточки: каждая отвечает своим обработчиком.
Добавьте карточку: она тоже заработает без нового кода.
Проверьте себя: чем event.target отличается от event.currentTarget?
target: где событие возникло. currentTarget: на каком элементе висит сработавший обработчик. При делегировании обработчик на контейнере (currentTarget), а нажали на элемент внутри (target).
Доступно с клавиатуры
Самая коварная ошибка с событиями: сделать так, что работает только мышь. Клавиатурой пользуются многие: люди с нарушениями моторики, те, кто слушает страницу экранным диктором, да и просто те, кому быстрее без мыши. Правила короткие.
- Используйте настоящий
button. Он получает фокус клавишей Tab, нажимается Enter и пробелом, объявляется диктором как «кнопка», и всё это бесплатно.divс обработчикомclickничего из этого не умеет, а ссылка<a href="#">, которая работает как кнопка, путает и тех, и других. - Сообщайте состояние. У кнопки, раскрывающей блок, есть атрибут
aria-expanded(falseилиtrue), и скрипт меняет его вместе с блоком. Атрибутaria-controlsназывает id управляемого блока. Диктор прочитает «Разделы, кнопка, свёрнуто». - Закрывайте по Escape и возвращайте фокус. Если меню открыли клавишей, фокус остался на кнопке. Закрывая меню, не теряйте его:
button.focus(). - Не убирайте видимый фокус. Рамка вокруг элемента, до которого дошёл Tab, это единственный способ понять, где вы находитесь.
В первом правиле есть и вторая половина: если без скрипта меню должно работать (а мы разберём почему в конце части), то список разделов в разметке виден всегда, а скрипт прячет его за кнопку. Не наоборот.
Практика
Продолжаем страницу эпохи 2014 года. Добавим два типичных элемента: меню, которое сворачивается за кнопку, и поиск по предложениям, который прямо при вводе скрывает лишние карточки.
Заметьте три детали в задании. Атрибут hidden проигрывает любому правилу CSS, которое задаёт display, поэтому нужно правило [hidden] { display: none !important }. Форма поиска получает обработчик submit с preventDefault, иначе Enter перезагрузит страницу. А карточки ищутся в момент ввода, а не один раз при запуске: список может поменяться, и вы увидите это в уроке про fetch.
Проверки, как и в прошлых уроках, читают разметку и текст скрипта. Работает ли он, вы проверяете сами: нажмите Tab до кнопки меню, откройте и закройте её клавишами, введите слово в поиск.
Что было не так раньше
В эпоху до addEventListener обработчики вешали по-разному: Internet Explorer до девятой версии использовал attachEvent, остальные браузеры addEventListener, и каждый разработчик писал свою обёртку (jQuery с её .click() и .on() была в том числе такой обёрткой). Меню делали из ссылок <a href="#">, у которых вместо адреса стоял «ничего»: они нажимались мышью, не объявлялись как кнопки и добавляли в адресную строку лишнюю решётку. Всплытие часто останавливали по привычке, и поэтому чужие скрипты (аналитика, например) переставали видеть клики.
Что осталось верным: слушать события лучше на общем родителе, не навешивая сто обработчиков, и считать клавиатуру равноправной с мышью.
Практика
Готовлю вашу страницу…
Тест
Вопрос 1 из 7
Карточки
Эти карточки попадут в повторение, когда вы завершите урок.
Как навесить обработчик события?
Ответ
element.addEventListener("click", функция). Функция получит объект события: у него есть type, target, key и другие поля. Атрибуты onclick в разметке не используют.
Что такое всплытие события?
Ответ
После срабатывания на элементе событие идёт вверх по предкам: карточка, контейнер, body, document. Каждый предок с обработчиком получит его. event.target остаётся тем, где всё началось.
Что такое делегирование и когда оно нужно?
Ответ
Один обработчик на общем родителе вместо многих на детях; внутри смотрят event.target или closest. Нужно, когда детей много или их добавляет скрипт позже.
Для чего preventDefault?
Ответ
Отменяет действие браузера по умолчанию: переход по ссылке, отправку формы. Для формы без него Enter перезагрузит страницу, и ваш код не успеет сработать.
Как сделать интерактивность доступной с клавиатуры?
Ответ
Использовать настоящие button и a, показывать фокус, менять aria-expanded вместе с состоянием, закрывать по Escape и возвращать фокус. Один только click на div этого не умеет.
Источники
- Введение в события (MDN) CC BY-SA, по-русски
- EventTarget.addEventListener (MDN) CC BY-SA, по-русски
- Введение в браузерные события (learn.javascript.ru) только ссылка, по-русски
- ARIA Authoring Practices Guide: Disclosure (W3C) только ссылка, по-английски
Видео
Уроки JavaScript для начинающих, №11: События и обработчик событий (Школа itProger)
Видео загрузится с youtube-nocookie.com только после нажатия.
Дальше
Теперь вы знаете: события и их всплытие.