W3docs

Слоты и композиция Shadow DOM

Слоты и композиция в JavaScript Shadow DOM: дефолтные и именованные слоты, light DOM и shadow DOM, выровненное дерево, событие slotchange и методы assignedNodes/assignedElements с примерами пользовательских элементов.

Слоты и композиция делают Shadow DOM по-настоящему переиспользуемым. Автор компонента один раз описывает фиксированную внутреннюю структуру, а потребители заполняют её своей разметкой — при этом оба уровня никогда не пересекаются. На этой странице рассматривается элемент <slot> (дефолтные и именованные слоты), то, как light DOM и shadow DOM объединяются в выровненное дерево (flattened tree), событие slotchange, а также методы assignedNodes() / assignedElements(), с помощью которых можно узнать, что попало в слот.

Если вы новичок в Shadow DOM, сначала прочитайте Shadow DOM для ознакомления с основами attachShadow() и теневыми корнями, а затем Web Components, чтобы понять, как слоты вписываются в концепцию рядом с пользовательскими элементами и шаблонами.

Light DOM и shadow DOM

Композиция включает два дерева:

  • Light DOM — разметка, которую пользователь пишет между тегами вашего элемента: <my-card>...эта часть...</my-card>. Она располагается в обычном документе и остаётся там.
  • Shadow DOM — разметка, которую вы подключаете с помощью attachShadow(). Она инкапсулирована и не доступна напрямую из внешнего документа.

<slot> — это окно: он находится в shadow DOM и проецирует дочерние элементы light DOM в нужное место. Узлы light DOM не перемещаются — они лишь отображаются в позиции слота. Это объединённое представление называется выровненным деревом (flattened tree) — именно его браузер отрисовывает и стилизует.

Понимание слотов в Shadow DOM

Слот — это заполнитель в вашем shadow DOM, куда браузер подставляет контент, переданный из light DOM. Слоты позволяют обобщённому компоненту выглядеть по-разному в каждом экземпляре, используя при этом единый внутренний шаблон.

Определение дефолтного слота

Используйте элемент <slot>. Слот без атрибута name является дефолтным слотом: он принимает любого потомка light DOM, не имеющего атрибута slot. Текст внутри <slot> — это резервный контент, отображаемый только тогда, когда ничего не назначено.

<body>
  <script>
    class CustomElement extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <div class="container">
            <slot>Default content</slot>
          </div>
        `;
      }
    }
    
    customElements.define('custom-element', CustomElement);
  </script>

  <!-- No children: the slot shows its fallback, "Default content" -->
  <custom-element></custom-element>

  <!-- Children with no slot attribute go into the default slot -->
  <custom-element><strong>Hello from the light DOM!</strong></custom-element>
</body>

Первый <custom-element> отображает «Default content», потому что ничего не было назначено. Второй отображает жирный текст — его потомок из light DOM заменяет резервный контент. Обратите внимание: разметка по-прежнему находится в документе; слот лишь отображает её.

Именованные слоты

Если компонент имеет более одной точки вставки, присвойте каждому <slot> атрибут name и укажите соответствие из light DOM с помощью атрибута slot="...". Именно так нужный контент попадает в нужное место.

<body>
  <!-- Define Custom Element -->
  <script>
    // Define Custom Element Class
    class CustomElement extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            /* Define styles for the component */
            .container {
              border: 1px solid #ccc;
              padding: 20px;
            }
          </style>
          <div class="container">
            <slot name="content">Default content</slot>
          </div>
        `;
      }
    }

    // Define Custom Element
    customElements.define('custom-element', CustomElement);
  </script>

  <!-- Displaying the custom element -->
  <custom-element>
    <div slot="content">Content from parent</div>
  </custom-element>
</body>

<div slot="content"> сопоставляется с <slot name="content">, поэтому «Content from parent» заменяет резервный контент. Всё, что не совпадает с именованным слотом, попадёт в дефолтный слот, если он существует, или просто не будет отрисовано.

Улучшение композиции в Shadow DOM

Композиция в контексте Shadow DOM означает сборку компонентов пользовательского интерфейса и контента путём объединения слотов и распределяемого содержимого для создания более сложных, переиспользуемых структур. Применяясь в контексте Shadow DOM, композиция позволяет создавать высококастомизируемые и переиспользуемые веб-компоненты.

Чтобы стилизовать контент, распределяемый в слоты из родительского элемента, используйте CSS-псевдоэлемент ::slotted() — например, ::slotted(div) { color: blue; }. Полная картина использования ::slotted(), :host и пользовательских CSS-свойств описана в разделе Shadow DOM Styling.

Компоновка компонентов со слотами

Один из мощных способов использования композиции — объединить несколько слотов в структурированный макет. Здесь составной компонент определяет области заголовка, содержимого и подвала:

<body>
  <script>
    // Define Composite Element Class
    class CompositeElement extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            /* Define styles for the composite component */
            .container {
              border: 1px solid #ccc;
              padding: 20px;
            }
          </style>
          <div class="container">
            <slot name="header"></slot>
            <slot name="content"></slot>
            <slot name="footer"></slot>
          </div>
        `;
      }
    }

    // Define Composite Element
    customElements.define('composite-element', CompositeElement);
  </script>
  <composite-element>
    <div slot="header">Header</div>
    <div slot="content">Content</div>
    <div slot="footer">Footer</div>
  </composite-element>
</body>

Каждый потомок с slot="..." направляется в соответствующий именованный слот, формируя аккуратный макет «заголовок/содержимое/подвал», который каждый экземпляр может заполнять по-своему.

Реакция на изменения слота с помощью slotchange

Содержимое слотов динамично: потребитель может добавлять, удалять или заменять потомков light DOM в любое время. Событие slotchange срабатывает на элементе <slot> всякий раз, когда изменяются назначенные ему узлы, — это позволяет компоненту реагировать: перерисовывать сводку, выполнять валидацию, загружать данные по запросу и т.д. Прослушивайте его изнутри теневого корня:

<body>
  <script>
    class TabList extends HTMLElement {
      constructor() {
        super();
        const root = this.attachShadow({ mode: 'open' });
        root.innerHTML = `<p id="count"></p><slot></slot>`;
        this._slot = root.querySelector('slot');
        this._count = root.querySelector('#count');
      }

      connectedCallback() {
        this._slot.addEventListener('slotchange', () => this.update());
        this.update();
      }

      update() {
        // assignedElements() returns only element nodes in the slot
        const items = this._slot.assignedElements();
        this._count.textContent = `Tabs: ${items.length}`;
      }
    }

    customElements.define('tab-list', TabList);
  </script>

  <tab-list>
    <button>One</button>
    <button>Two</button>
  </tab-list>
  <script>
    // Adding a child later fires slotchange → count updates to 3
    const list = document.querySelector('tab-list');
    const extra = document.createElement('button');
    extra.textContent = 'Three';
    list.appendChild(extra);
  </script>
</body>

Изначально компонент отображает «Tabs: 2». Когда добавляется третья кнопка <button>, срабатывает slotchange и счётчик обновляется до «Tabs: 3».

Чтение содержимого слота: assignedNodes() и assignedElements()

Оба метода вызываются на элементе <slot> и возвращают то, что браузер назначил ему из light DOM:

  • slot.assignedNodes() возвращает все узлы — как элементы, так и текстовые узлы (включая пробельные символы между тегами).
  • slot.assignedElements() возвращает только элементные узлы. Как правило, именно это вам и нужно.

Передайте { flatten: true }, чтобы обойти вложенные слоты, когда слоты выстроены в цепочку между компонентами:

// All nodes, including stray text/whitespace nodes
slot.assignedNodes();           // e.g. [text, <button>, text, <button>, text]

// Elements only — cleaner for iteration
slot.assignedElements();        // e.g. [<button>, <button>]

// Flatten through nested <slot> assignments
slot.assignedElements({ flatten: true });

Предпочитайте assignedElements(), если только вам специально не нужны текстовые узлы — это избавляет от необходимости фильтровать пробельные символы.

Выровненное дерево: резюме

Браузер буквально не перемещает узлы light DOM в shadow DOM. Вместо этого он строит выровненное дерево (flattened tree), подставляя в каждый слот назначенные ему узлы для отрисовки и стилизации. Практические следствия:

  • Элементы слота остаются в документе, поэтому document.querySelector() по-прежнему их находит, а их исходные class и id продолжают действовать.
  • Они стилизуются CSS страницы, тогда как компонент обращается к ним только через ::slotted().
  • Обработчики событий, прикреплённые в light DOM, продолжают работать — события всплывают через выровненное дерево.

Заключение

Слоты и композиция превращают инкапсулированный shadow DOM в гибкий, переиспользуемый компонент: вы определяете структуру, а потребители наполняют её контентом через дефолтные и именованные слоты. Запомните ключевые концепции: light DOM и shadow DOM, выровненное дерево, которое отрисовывает браузер, событие slotchange для реакции на изменения и assignedElements() для чтения содержимого слота.

Для дальнейшего изучения смотрите Web Components — общая картина, Custom Elements — жизненный цикл элементов, Shadow DOM Styling::slotted() и :host, а также Shadow DOM — основы.

Практика

Практика
Для чего используются слоты в JavaScript Shadow DOM?
Для чего используются слоты в JavaScript Shadow DOM?
Was this page helpful?