Kodokon kodokon.com

Веб-компоненты: пользовательские элементы, теневой DOM и слоты

Освой жизненный цикл пользовательского элемента, инкапсуляцию теневого DOM и проекцию содержимого через слоты.

12 мин · 3 вопросов

Открыть этот урок в Kodokon

Пользовательские элементы расширяют словарь HTML: любой класс, наследующий от HTMLElement, можно зарегистрировать в CustomElementRegistry, если его имя содержит дефис (user-badge, но никогда userbadge). Тонкий момент: если тег появляется в документе до вызова customElements.define, он сначала создаётся как неизвестный элемент, а затем обновляется на месте (upgrade) - конструктор в этом случае вызывается задним числом на узле, уже присутствующем в дереве.

JAVASCRIPT
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 или клонирование) элемент может быть совершенно пустым.

HTML
<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 уведомляет тебя о любом изменении назначения.

JAVASCRIPT
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, что делает веб-компоненты совместимыми с рендерингом на сервере.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему конструктор пользовательского элемента не должен ни читать свои атрибуты, ни создавать потомков?
    • Потому что до connectedCallback атрибуты заморожены и доступны только для чтения
    • Потому что во время конструирования теневой DOM скрывает светлый DOM
    • Потому что в этот момент (createElement, клонирование) элемент может быть пустым; спецификация требует конструктор без влияния на содержимое
  2. Что возвращает снаружи element.shadowRoot при attachShadow({ mode: 'closed' })?
    • null: доступ к теневому корню даёт только ссылка, возвращённая attachShadow
    • Теневой корень, но только для чтения
    • Пустой DocumentFragment
    • Выбрасывается DOMException
  3. Где физически находятся узлы, спроецированные в <slot>?
    • Они перемещаются в теневой DOM во время отрисовки
    • Они остаются в светлом DOM; слот лишь показывает их на своей позиции в сплющенном дереве
    • Они клонируются: оригинал остаётся в светлом DOM, а копия живёт в теневом DOM