Освой жизненный цикл пользовательского элемента, инкапсуляцию теневого DOM и проекцию содержимого через слоты.
Открыть этот урок в KodokonПользовательские элементы расширяют словарь HTML: любой класс, наследующий от HTMLElement, можно зарегистрировать в CustomElementRegistry, если его имя содержит дефис (user-badge, но никогда userbadge). Тонкий момент: если тег появляется в документе до вызова customElements.define, он сначала создаётся как неизвестный элемент, а затем обновляется на месте (upgrade) - конструктор в этом случае вызывается задним числом на узле, уже присутствующем в дереве.
class UserBadge extends HTMLElement {
static observedAttributes = ["name"];
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
this.textContent = this.getAttribute("name");
}
}
customElements.define("user-badge", UserBadge);В жизненном цикле есть свои ловушки: attributeChangedCallback срабатывает только для атрибутов, перечисленных в observedAttributes, а во время обновления (upgrade) он вызывается для каждого уже присутствующего атрибута до connectedCallback. Спецификация запрещает конструктору изучать атрибуты и потомков, а также добавлять их: в момент его выполнения (через createElement или клонирование) элемент может быть совершенно пустым.
<template id="card-tpl">
<style>
:host { display: block; padding: 1rem; }
::slotted(h2) { margin: 0; }
</style>
<slot name="title"></slot>
<slot></slot>
</template>attachShadow создаёт инкапсулированное поддерево: стили из него не вытекают, селекторы страницы не достают внутрь - границу пересекают только наследуемые свойства CSS (color, font) и пользовательские свойства. Содержимое <template> живёт в DocumentFragment, доступном через .content: его скрипты не выполняются, а изображения не загружаются, пока ты не клонируешь его в документ. Узлы, размещённые в светлом DOM, проецируются в элементы <slot>: физически они остаются в светлом DOM, а событие slotchange уведомляет тебя о любом изменении назначения.
class InfoCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById("card-tpl");
this.attachShadow({ mode: "open" })
.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define("info-card", InfoCard);Два расширения заслуживают твоего внимания. Пользовательские элементы, связанные с формой (form-associated custom elements): с static formAssociated = true и attachInternals() твой компонент полноценно участвует в формах (отправляемое значение, встроенная валидация, состояния :invalid). И декларативный теневой DOM: <template shadowrootmode=open> присоединяет теневой корень прямо во время разбора, без JavaScript, что делает веб-компоненты совместимыми с рендерингом на сервере.
element.shadowRoot при attachShadow({ mode: 'closed' })?null: доступ к теневому корню даёт только ссылка, возвращённая attachShadow<slot>?