Обработайте click внутренней кнопки, затем отправьте именованное событие CustomEvent от хоста компонента. Страница-потребитель прослушивает это событие на хосте и читает event.detail. В этом руководстве создаётся полноценный локальный пример для браузера на Mac и объясняется, какие параметры события нужны каждому слушателю.
Определите уведомление, которое нужно потребителю
В обычном веб-компоненте на JavaScript потребитель — это страница или внешний компонент, использующий ваш пользовательский элемент. В данном случае задача состоит в том, чтобы позволить потребителю реагировать на нажатие внутренней кнопки, не обращаясь к теневому DOM компонента.
В примере используется условная плитка с меткой Tile A. Её публичное событие — tile-activate, которое содержит itemId и номер активации count. CustomEvent предоставляет поле detail для этих данных приложения; имя события чувствительно к регистру. Код отправки и слушатель должны использовать в точности одно и то же имя. Документация по конструктору CustomEvent.
Считайте эти решения небольшим контрактом интерфейса:
- Имя:
tile-activate. - Значение: внутренняя кнопка активирована, а счётчик обновлён.
- Данные:
detail.itemIdуказывает на плитку;detail.countсодержит обновлённое значение счётчика. - Источник: хост пользовательского элемента отправляет уведомление.
- Доставка: его получают непосредственные потребители и слушатели на предках, в том числе через границы внешних теневых DOM.
Это решения для данного примера. Полезное событие описывает действие, которое важно потребителю. Благодаря этому компонент может позднее изменить внутреннюю разметку, сохранив тот же интерфейс уведомлений.
Создайте и откройте полный пример на Mac
Вам понадобится современный настольный браузер и обычный текстовый редактор. Для этого примера в одном файле не требуется ни устанавливать пакеты, ни выполнять сборку. В нём используется автономный пользовательский элемент, расширяющий HTMLElement, — шаблон платформы, описанный в разделе о пользовательских элементах стандарта HTML.
В TextEdit выберите File > New, затем Format > Make Plain Text. Вставьте полный документ ниже. Выберите File > Save, задайте имя component-click.html и при запросе выберите Use .html. Это шаги Apple для создания HTML-файла в TextEdit. Работа с HTML-документами в TextEdit на Mac.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Component click notification</title>
</head>
<body>
<h1>Component click notification</h1>
<section id="demo">
<action-tile id="tile" item-id="tile-a"></action-tile>
<p id="direct-status">Direct listener: waiting</p>
<p id="parent-status">Parent listener: waiting</p>
</section>
<script>
class ActionTile extends HTMLElement {
constructor() {
super();
this._count = 0;
this._button = null;
this._root = this.attachShadow({ mode: "open" });
this._handleClick = () => {
this._count += 1;
this.dispatchEvent(new CustomEvent("tile-activate", {
detail: {
itemId: this.getAttribute("item-id"),
count: this._count
},
bubbles: true,
composed: true
}));
};
}
connectedCallback() {
if (!this._button) {
this._button = document.createElement("button");
this._button.type = "button";
this._button.textContent = "Activate Tile A";
this._root.append(this._button);
}
this._button.addEventListener("click", this._handleClick);
}
disconnectedCallback() {
this._button.removeEventListener("click", this._handleClick);
}
}
customElements.define("action-tile", ActionTile);
const tile = document.getElementById("tile");
const demo = document.getElementById("demo");
const directStatus = document.getElementById("direct-status");
const parentStatus = document.getElementById("parent-status");
tile.addEventListener("tile-activate", (event) => {
const { itemId, count } = event.detail;
directStatus.textContent =
`Direct listener: ${itemId}, activation ${count}`;
});
demo.addEventListener("tile-activate", (event) => {
const { itemId, count } = event.detail;
parentStatus.textContent =
`Parent listener: ${itemId}, activation ${count}`;
});
</script>
</body>
</html>
Откройте сохранённый файл в браузере и нажмите Activate Tile A. Вначале должны появиться строки Direct listener: tile-a, activation 1 и Parent listener: tile-a, activation 1. При каждом следующем нажатии оба отображаемых счётчика увеличиваются на единицу. Это ожидаемый результат примера, а не отчёт о результатах тестирования в браузере.
В коде видна важная последовательность: обновить состояние компонента, создать уведомление, отправить его от this, а затем позволить потребителю обновить собственный интерфейс. dispatchEvent() синхронно вызывает подходящие слушатели до возврата, поэтому в обработчиках потребителей уже будет виден обновлённый счётчик. Документация по dispatchEvent.
Выберите нужные параметры всплытия и пересечения границ теневого DOM
На нужные параметры влияют цель отправки события и расположение слушателя. Слушатель, непосредственно подключённый к хосту, получит событие, отправленное на этот хост, даже если bubbles имеет значение false. Обычному слушателю на предке нужно всплытие события. Для пересечения границы теневого DOM также требуется composed. По умолчанию для bubbles, composed и cancelable заданы значения false. Стандарт DOM: интерфейс Event.
В таблице ниже приведены варианты, выведенные из этих правил распространения. Предполагается, что обычные слушатели зарегистрированы без фазы захвата и ни один обработчик не останавливает распространение.
| Где начинается пользовательское событие | Где слушает потребитель | Настройка для этого пути |
|---|---|---|
| Хост компонента | Тот же хост | Ни один флаг не должен иметь значение true |
| Хост компонента | Обычный предок в том же дереве | bubbles: true |
| Внутренняя кнопка в теневом DOM | Обычный предок за пределами этого теневого корня | bubbles: true, composed: true |
| Хост компонента внутри другого теневого дерева | Обычный предок за пределами охватывающего теневого корня | bubbles: true, composed: true |
В примере this.dispatchEvent(...) отправляет уведомление на хост. Поэтому ему не нужно выходить за пределы собственного теневого корня компонента. composed: true позволяет использовать уведомление через границу теневого DOM внешнего компонента, если эта плитка позднее окажется внутри него. Такой более широкий охват должен быть осознанной частью публичного интерфейса. Документация по свойству composed события.
Для небольшого эксперимента измените только bubbles: true на bubbles: false, сохраните файл и перезагрузите страницу. При нажатии статус прямого слушателя должен обновиться, а статус родительского останется Parent listener: waiting. После этого верните всплытие. Если изменить только composed на false, в этом документе оба слушателя продолжат работать, поскольку ни один путь не пересекает границу теневого DOM.
Сохраните корректную инициализацию и поведение при повторном подключении
Поначалу доставка событий может работать правильно, но после перемещения или повторного подключения компонента появятся дублирующиеся уведомления. Чётко разграничьте создание, подключение и очистку.
Конструктор сначала вызывает super(), затем инициализирует состояние и присоединяет теневой корень. Отрисовка выполняется в connectedCallback() и защищена проверкой существующей ссылки на кнопку, чтобы запускаться один раз. Эти решения соответствуют требованиям стандарта HTML к конструкторам и его рекомендациям защищать инициализацию, которая не должна повторяться при повторном вызове колбеков подключения. Требования к конструкторам пользовательских элементов и реакциям.
Компонент регистрирует слушатель щелчка внутренней кнопки при подключении и удаляет его при отключении. При повторном подключении он повторно использует существующую кнопку и сохраняет _count. Колбеки подключения и отключения — это хуки жизненного цикла, вызываемые, когда элементы входят в документ и покидают его. Использование пользовательских элементов.
Сохранённая функция _handleClick важна: при удалении нужно указать ту же функцию, что использовалась при регистрации, с тем же параметром захвата. Созданная заново стрелочная функция в вызове удаления не будет соответствовать зарегистрированному слушателю. Документация по removeEventListener.
Если в более крупном компоненте вы добавляете слушатели для window, document или другой внешней цели, предусмотрите для них столь же чёткую очистку. Отделяйте изменения жизненного цикла от значения уведомления, чтобы повторное подключение элемента случайно не объявляло о действии пользователя.
Найдите причину пропущенных или повторных уведомлений
Прежде чем менять флаги события, используйте две строки статуса, чтобы определить источник сбоя. Они позволяют отличить проблему с уведомлением компонента от проблемы с доставкой на предков.
- Не меняется ни один статус: сравните имя события при отправке и регистрации, затем убедитесь, что определение пользовательского элемента и обработчик кнопки выполняются. В этом документе слушатели потребителя регистрируются до любого нажатия.
- Меняется только прямой статус: проверьте
bubbles. Также изучите все промежуточные обработчики, которые могут останавливать распространение. - Событие не доставляется только при вложении в другой компонент: найдите границу теневого DOM на этом пути и проверьте
composed. - После повторного подключения значения счётчиков резко возрастают: проверьте, не регистрируются ли слушатели повторно, особенно новые анонимные функции при каждом подключении элемента.
Повторно созданные анонимные функции — это разные объекты слушателей, даже если исходный текст у них одинаковый. Стабильная ссылка на функцию упрощает анализ регистрации и очистки. addEventListener() также задаёт параметр захвата слушателя, влияющий на момент его выполнения. Документация по addEventListener.
Возможно, что нативный click уже выйдет за пределы теневого дерева, поскольку браузерные события щелчка являются составными. Отдельное пользовательское событие полезно, если потребителю нужно именованное действие компонента и определённые данные. Не обрабатывайте оба уведомления как одно и то же действие приложения, если намеренно не хотите такого дублирования. Документация по свойству composed события.
Наконец, в дизайне событий разделяйте уведомление и разрешение на продолжение действия. В этом примере счётчик увеличивается до отправки события, и событие не является отменяемым. Вызов preventDefault() потребителем не отменит обновление. Для отменяемого запроса нужна явная реализация, которая проверяет возвращаемое значение dispatchEvent() перед выполнением запрошенного действия. Возвращаемое значение dispatchEvent.
Для этой задачи контракт прост: один обработчик внутренней кнопки обновляет состояние, одно именованное пользовательское событие отправляется от хоста, а потребители читают документированные данные. Сначала убедитесь в работе прямого слушателя, а затем включайте те пути распространения, которые действительно нужны содержащему этот компонент интерфейсу.
Источники
- Стандарт HTML — Пользовательские элементы — WHATWG.
- Стандарт DOM — WHATWG.
- CustomEvent: конструктор CustomEvent() — MDN Web Docs.
- EventTarget: метод dispatchEvent() — MDN Web Docs.
- Event: свойство composed — MDN Web Docs.
- Использование пользовательских элементов — MDN Web Docs.
- EventTarget: метод addEventListener() — MDN Web Docs.
- EventTarget: метод removeEventListener() — MDN Web Docs.
- Работа с HTML-документами в TextEdit на Mac — Apple.