Можно не только назначать обработчики, но и генерировать события из JavaScript-кода.
Пользовательские события могут быть использованы при создании графических компонентов. Например, корневой элемент нашего меню, реализованного при помощи JavaScript, может генерировать события, относящиеся к этому меню: open
(меню раскрыто), select
(выбран пункт меню) и т.п. А другой код может слушать эти события и узнавать, что происходит с меню.
Можно генерировать не только совершенно новые, придуманные нами события, но и встроенные, такие как click
, mousedown
и другие. Это бывает полезно для автоматического тестирования.
Конструктор Event
Встроенные классы для событий формируют иерархию аналогично классам для DOM-элементов. Её корнем является встроенный класс Event.
Событие встроенного класса Event
можно создать так:
let
event =
new
Event
(
type[
,
options]
)
;
Где:
- type – тип события, строка, например
"click"
или же любой придуманный нами –"my-event"
. - options – объект с тремя необязательными свойствами:
bubbles: true/false
– еслиtrue
, тогда событие всплывает.cancelable: true/false
– еслиtrue
, тогда можно отменить действие по умолчанию. Позже мы разберём, что это значит для пользовательских событий.composed: true/false
– еслиtrue
, тогда событие будет всплывать наружу за пределы Shadow DOM. Позже мы разберём это в разделе Веб-компоненты.
По умолчанию все три свойства установлены в false: {bubbles: false, cancelable: false, composed: 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)
;
// обработчик на document сработает и выведет сообщение.
</
script
>
Обратите внимание:
- Мы должны использовать
addEventListener
для наших собственных событий, т.к.on<event>
-свойства существуют только для встроенных событий, то естьdocument.onhello
не сработает. - Мы обязаны передать флаг
bubbles:true
, иначе наше событие не будет всплывать.
Механизм всплытия идентичен как для встроенного события (click
), так и для пользовательского события (hello
). Также одинакова работа фаз всплытия и погружения.
MouseEvent, KeyboardEvent и другие
Для некоторых конкретных типов событий есть свои специфические конструкторы. Вот небольшой список конструкторов для различных событий пользовательского интерфейса, которые можно найти в спецификации UI Event:
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
, а свойства записать в объект отдельно, после создания, вот так: 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"
, уведомляя всех интересующихся, что кролик собирается спрятаться.
Любой обработчик может узнать об этом, подписавшись на событие hide
через rabbit.addEventListener('hide',...)
и, при желании, отменить действие по умолчанию через event.preventDefault()
. Тогда кролик не исчезнет:
<
pre
id
=
"
rabbit"
>
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</
pre
>
<
button
onclick
=
"
hide
(
)
"
>
Hide()</
button
>
<
script
>
// hide() будет вызван при щелчке на кнопке
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
и в процессе этого произойдёт новое событие, то оно ждёт, пока закончится обработка onclick
.
Исключением является ситуация, когда событие инициировано из обработчика другого события.
Тогда управление сначала переходит в обработчик вложенного события и уже после этого возвращается назад.
В примере ниже событие menu-open
обрабатывается синхронно во время обработки onclick
:
<
button
id
=
"
menu"
>
Меню (нажми меня)</
button
>
<
script
>
menu.
onclick
=
function
(
)
{
alert
(
1
)
;
// alert("вложенное событие")
menu.
dispatchEvent
(
new
CustomEvent
(
"menu-open"
,
{
bubbles
:
true
}
)
)
;
alert
(
2
)
;
}
;
document.
addEventListener
(
'menu-open'
,
(
)
=>
alert
(
'вложенное событие'
)
)
</
script
>
Порядок вывода: 1 → вложенное событие → 2.
Обратите внимание, что вложенное событие menu-open
успевает всплыть и запустить обработчик на document
. Обработка вложенного события полностью завершается до того, как управление возвращается во внешний код (onclick
).
Это справедливо не только для dispatchEvent
, но и для других ситуаций. JavaScript в обработчике события может вызвать другие методы, которые приведут к другим событиям – они тоже обрабатываются синхронно.
Если нам это не подходит, то мы можем либо поместить dispatchEvent
(или любой другой код, инициирующий события) в конец обработчика onclick
, либо, если это неудобно, можно обернуть генерацию события в setTimeout
с нулевой задержкой:
<
button
id
=
"
menu"
>
Меню (нажми меня)</
button
>
<
script
>
menu.
onclick
=
function
(
)
{
alert
(
1
)
;
// alert(2)
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)
принимает обязательное имя события и options
– объект с двумя свойствами:
bubbles: true
чтобы событие всплывало.cancelable: true
если мы хотим, чтобыevent.preventDefault()
работал.
Особые конструкторы встроенных событий MouseEvent
, KeyboardEvent
и другие принимают специфичные для каждого конкретного типа событий свойства. Например, clientX
для событий мыши.
Для пользовательских событий стоит применять конструктор CustomEvent
. У него есть дополнительная опция detail
, с помощью которой можно передавать информацию в объекте события. После чего все обработчики смогут получить к ней доступ через event.detail
.
Несмотря на техническую возможность генерировать встроенные браузерные события типа click
или keydown
, пользоваться ей стоит с большой осторожностью.
Весьма часто, когда разработчик хочет сгенерировать встроенное событие – это вызвано «кривой» архитектурой кода.
Как правило, генерация встроенных событий полезна в следующих случаях:
- Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.
- Либо для автоматического тестирования, чтобы скриптом «нажать на кнопку» и посмотреть, произошло ли нужное действие.
Пользовательские события со своими именами часто создают для улучшения архитектуры, чтобы сообщить о том, что происходит внутри наших меню, слайдеров, каруселей и т.д.
Комментарии
<code>
, для нескольких строк кода — тег<pre>
, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)