Ми можемо не тільки призначати обробники, але й генерувати події з JavaScript.
Користувацькі події можна використовувати для створення “графічних компонентів”. Наприклад, кореневий елемент нашого власного меню на основі JS може викликати події, які повідомлять, що відбувається з меню: open
(меню відкрито), select
(вибрано елемент) тощо. Інший код може прослуховувати ці події та дізнаватись що відбувається з меню.
Ми можемо генерувати не тільки абсолютно нові події, які ми вигадуємо для власних цілей, але й вбудовані, такі як click
, mousedown
тощо. Це може бути корисно для автоматизованого тестування.
Конструктор подій
Вбудовані класи подій утворюють ієрархію, подібну до класів елементів DOM. Корінь – це вбудований клас Event.
Ми можемо створювати об’єкти Event
на зразок цього:
let event = new Event(type[, options]);
Аргументи:
-
type – тип події, рядок, як-от
"click"
або наш власний, наприклад,"my-event"
. -
options – об’єкт з двома необов’язковими властивостями:
bubbles: true/false
– якщоtrue
, то подія спливає.cancelable: true/false
– якщоtrue
, то “типова дія” може бути попереджена. Пізніше ми побачимо, що це означає для користувацьких подій.
Типово обидва параметри є хибними:
{bubbles: false, cancelable: false}
.
dispatchEvent
Після створення об’єкта події ми повинні “запустити” її на елементі за допомогою виклику elem.dispatchEvent(event)
.
Потім обробники реагують на неї, як на звичайну подію браузера. Якщо подія була створена з прапором bubbles
, вона спливає.
У наведеному нижче прикладі подія click
ініціюється в JavaScript. Обробник працює так само, як якщо б клікнули кнопку:
<button id="elem" onclick="alert('Клік!');">Автоклік</button>
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
Існує спосіб відрізнити “справжню” користувацьку подію від такої, яку згенеровано скриптом.
Властивість event.isTrusted
має значення true
для подій, які відбуваються в результаті реальних дій користувача, і false
для подій, згенерованих скриптом.
Приклад спливання
Ми можемо створити спливаючу подію з назвою "hello"
і зловити її на document
.
Все, що нам потрібно, це встановити bubbles
на true
:
<h1 id="elem">Привіт від скрипта!</h1>
<script>
// ловимо на document...
document.addEventListener("hello", function(event) { // (1)
alert("Привіт від " + event.target.tagName); // Привіт від H1
});
// ...запуск події на елементі!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
// обробник документа активується та відобразить повідомлення.
</script>
Примітки:
- Нам слід використовувати
addEventListener
для наших користувацьких подій, оскількиon<event>
існує лише для вбудованих подій,document.onhello
не працює. - Потрібно встановити
bubbles:true
, інакше подія не спливе.
Механіка спливання однакова для вбудованих (click
) і користувацьких (hello
) подій. Також є етапи перехоплення та спливання.
MouseEvent, KeyboardEvent та інші
Ось короткий список класів для подій інтерфейсу користувача з UI Event specification:
UIEvent
FocusEvent
MouseEvent
WheelEvent
KeyboardEvent
- …
Ми повинні використовувати їх замість new Event
, якщо ми хочемо створити такі події. Наприклад, new MouseEvent("click")
Правильний конструктор дозволяє вказати стандартні властивості для цього типу події.
Такі як clientX/clientY
для події миші:
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // 100
Зверніть увагу: цього не можна було б зробити за допомогою базового конструктора Event
.
Давайте спробуємо:
let event = new Event("click", {
bubbles: true, // тільки властивості bubbles та cancelable
cancelable: true, // працюють в Event конструкторі
clientX: 100,
clientY: 100
});
alert(event.clientX); // undefined, невідома властивість ігнорується!
Втім, ми можемо обійти це, призначивши event.clientX=100
безпосередньо після створення об’єкта. Тож це питання зручності та дотримання правил. Події, створені браузером, завжди мають правильний тип.
Повний опис властивостей для різних події інтерфейсу користувача є в специфікації, наприклад, MouseEvent.
Користувацькі події
Для наших власних, абсолютно нових типів подій, таких як "hello"
, ми повинні використовувати new CustomEvent
. Технічно CustomEvent – це те ж саме, що й Event
, за одним винятком.
У другий аргумент (об’єкт) ми можемо додати додаткову властивість detail
для будь-якої спеціальної інформації, яку ми хочемо передати разом із подією.
Наприклад:
<h1 id="elem">Привіт від Івана!</h1>
<script>
// додаткові відомості надходять разом із подією до обробника
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "Іван" }
}));
</script>
Властивість detail
може містити будь-які дані. Технічно ми могли б жити і без них, оскільки ми можемо призначити будь-які властивості звичайному об’єкту new Event
після його створення. Але CustomEvent
забезпечує спеціальне поле detail
, щоб уникнути конфліктів з іншими властивостями події.
Крім того, клас події описує яка саме це подія, і якщо вона користувацька, то ми повинні використовувати CustomEvent
, щоб явно вказати на це.
event.preventDefault()
Багато браузерів, які мають “типові дії”, як-от перехід за посиланням, виділення тощо.
Для нових користувацьких подій, безумовно, немає типових дій браузера, але код, який надсилає таку подію, може мати власні плани, що робити після ініціювання події.
Викликаючи event.preventDefault()
, обробник події може надіслати сигнал про те, що ці дії слід скасувати.
У цьому випадку виклик elem.dispatchEvent(event)
повертає false
. І код, який його надіслав, знає, що продовжувати не потрібно.
Давайте подивимося на практичний приклад – кролик, що ховається (могло б бути меню, що закривається, або щось подібне).
Нижче ви можете побачити функції #rabbit
і hide()
, які запускають подію "hide"
, щоб усі зацікавлені сторони знали, що кролик збирається сховатися.
Будь-який обробник може прослухати цю подію за допомогою rabbit.addEventListener('hide',...)
і, якщо потрібно, скасувати дію за допомогою event.preventDefault()
. Тоді кролик не зникне:
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>
<script>
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // без цього прапорця preventDefault не спрацює
});
if (!rabbit.dispatchEvent(event)) {
alert('Обробник запобіг дії');
} else {
rabbit.hidden = true;
}
}
rabbit.addEventListener('hide', function(event) {
if (confirm("Викликати preventDefault?")) {
event.preventDefault();
}
});
</script>
Зверніть увагу: подія повинна мати прапор cancelable: true
, інакше виклик event.preventDefault()
ігнорується.
Вкладені події є синхронними
Як правило, події обробляються в черзі. Тобто: якщо браузер обробляє onclick
і відбувається нова подія, напр. курсор було переміщено, тоді її обробка ставиться в чергу, відповідні обробники mousemove
будуть викликані після завершення обробки onclick
.
Винятком є випадки, коли одна подія починається з іншої, напр. використовуючи dispatchEvent
. Такі події обробляються негайно: викликаються нові обробники подій, а потім відновлюється обробка поточної події.
Наприклад, у коді нижче подія menu-open
ініціюється під час onclick
.
Вона обробляється негайно, не чекаючи закінчення обробки onclick
:
<button id="menu">Меню (клікни мене)</button>
<script>
menu.onclick = function() {
alert(1);
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
// спрацьовує між 1 та 2
document.addEventListener('menu-open', () => alert('вкладена подія'));
</script>
Порядок виведення такий: 1 → вкладена подія → 2.
Зауважте, що вкладена подія menu-open
ловиться на document
. Поширення та обробка вкладеної події закінчується до того, як опрацювання повернеться до зовнішнього коду (onclick
).
Це стосується не тільки dispatchEvent
, є й інші випадки. Якщо обробник подій викликає методи, які викликають інші події, вони також обробляються синхронно, вкладеним способом.
Скажімо, нам це не подобається. Ми б хотіли спочатку повністю обробити onclick
, незалежно від menu-open
або будь-яких інших вкладених подій.
Тоді ми можемо або помістити dispatchEvent
(або інший виклик, що ініціює подію) в кінець onclick
, або, можливо, краще, загорнути його в setTimeout
з нульовою затримкою:
<button id="menu">Меню (клікни мене)</button>
<script>
menu.onclick = function() {
alert(1);
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
alert(2);
};
document.addEventListener('menu-open', () => alert('вкладена подія'));
</script>
Тепер dispatchEvent
запускається асинхронно після завершення поточного виконання коду, включаючи menu.onclick
, тому обробники подій повністю відокремлені.
Порядок виведення стає: 1 → 2 → вкладена подія.
Підсумки
Щоб створити подію з коду, нам спочатку потрібно створити об’єкт події.
Базовий конструктор Event(name, options)
приймає довільне ім’я події та об’єкт параметрів із двома властивостями:
bubbles: true
якщо подія має спливати.cancelable: true
якщоevent.preventDefault()
повинен працювати.
Інші конструктори вбудованих подій, як-от MouseEvent
, KeyboardEvent
тощо, приймають властивості, характерні для цього типу події. Наприклад, clientX
для подій миші.
Для користувацьких подій ми повинні використовувати конструктор CustomEvent
. Він має додаткову опцію з назвою detail
, ми повинні призначити їй дані, що стосуються події. Тоді всі обробники зможуть отримати до них доступ як event.detail
.
Незважаючи на технічну можливість генерування подій браузера, таких як click
або keydown
, ми повинні користуватись цим з великою обережністю.
Ми не повинні генерувати події браузера, оскільки це хакерський спосіб запуску обробників. Найчастіше це ознака поганої архітектури.
Вбудовані події можуть бути створені:
- Як явний хак, щоб змусити сторонні бібліотеки працювати належним чином, якщо вони не забезпечують інших засобів взаємодії.
- Для автоматизованого тестування, щоб “клікнути кнопку” в скрипті та перевірити, чи правильно реагує інтерфейс.
Користувацькі події з нашими власними назвами найчастіше генеруються для архітектурних цілей, щоб сигналізувати про те, що відбувається в наших меню, повзунках, каруселях тощо.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)