Можно не только назначать обработчики, но и генерировать события из 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
запускается асинхронно после исполнения текущего кода, включая mouse.onclick
, поэтому обработчики полностью независимы.
Новый порядок вывода: 1 → 2 → вложенное событие.
Итого
Чтобы сгенерировать событие из кода, вначале надо создать объект события.
Базовый конструктор Event(name, options)
принимает обязательное имя события и options
– объект с двумя свойствами:
bubbles: true
чтобы событие всплывало.cancelable: true
если мы хотим, чтобыevent.preventDefault()
работал.
Особые конструкторы встроенных событий MouseEvent
, KeyboardEvent
и другие принимают специфичные для каждого конкретного типа событий свойства. Например, clientX
для событий мыши.
Для пользовательских событий стоит применять конструктор CustomEvent
. У него есть дополнительная опция detail
, с помощью которой можно передавать информацию в объекте события. После чего все обработчики смогут получить к ней доступ через event.detail
.
Несмотря на техническую возможность генерировать встроенные браузерные события типа click
или keydown
, пользоваться ей стоит с большой осторожностью.
Весьма часто, когда разработчик хочет сгенерировать встроенное событие – это вызвано «кривой» архитектурой кода.
Как правило, генерация встроенных событий полезна в следующих случаях:
- Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.
- Либо для автоматического тестирования, чтобы скриптом «нажать на кнопку» и посмотреть, произошло ли нужное действие.
Пользовательские события со своими именами часто создают для улучшения архитектуры, чтобы сообщить о том, что происходит внутри наших меню, слайдеров, каруселей и т.д.