Шаблон, слот и тень

Преимущество веб-компонентов заключается в возможности их повторного использования: вы можете создать виджет пользовательского интерфейса один раз и повторно использовать его несколько раз. Хотя для создания веб-компонентов вам нужен JavaScript, библиотека JavaScript вам не нужна. HTML и связанные с ним API предоставляют все, что вам нужно.

Стандарт веб-компонентов состоит из трех частей — шаблонов HTML , пользовательских элементов и теневого DOM . В совокупности они позволяют создавать настраиваемые, автономные (инкапсулированные) элементы многократного использования, которые можно легко интегрировать в существующие приложения, как и все другие элементы HTML, которые мы уже рассмотрели.

В этом разделе мы создадим элемент <star-rating> — веб-компонент, который позволяет пользователям оценивать опыт по шкале от одной до пяти звезд. При названии пользовательского элемента рекомендуется использовать только строчные буквы. Также добавьте тире, поскольку оно помогает различать обычные и пользовательские элементы.

Мы обсудим использование элементов <template> и <slot> , атрибута slot и JavaScript для создания шаблона с инкапсулированным Shadow DOM . Затем мы повторно используем определенный элемент, настраивая часть текста, как и любой другой элемент или веб-компонент. Мы также кратко обсудим использование CSS внутри и снаружи пользовательского элемента.

Элемент <template>

Элемент <template> используется для объявления фрагментов HTML, которые будут клонированы и вставлены в DOM с помощью JavaScript. Содержимое элемента не отображается по умолчанию. Скорее, они создаются с использованием JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Поскольку содержимое элемента <template> не выводится на экран, <form> и его содержимое не отображаются. Да, этот Codepen пуст, но если вы посмотрите вкладку HTML, вы увидите разметку <template> .

В этом примере <form> не является дочерним элементом <template> в DOM. Скорее, содержимое элементов <template> является дочерним элементом DocumentFragment возвращаемого свойством HTMLTemplateElement.content . Чтобы сделать его видимым, необходимо использовать JavaScript для захвата содержимого и добавления его в DOM.

В этом кратком JavaScript-коде не создается пользовательский элемент. Скорее, в этом примере содержимое <template> добавлено в <body> . Содержимое стало частью видимой, стилизованной модели DOM.

Скриншот предыдущего кода, показанного в DOM.

Требовать JavaScript для реализации шаблона только для одного звездного рейтинга не очень полезно, но полезно создать веб-компонент для многократно используемого настраиваемого виджета звездного рейтинга.

Элемент <slot>

Мы включили слот для включения индивидуальной легенды для каждого события. HTML предоставляет элемент <slot> в качестве заполнителя внутри <template> , который, если ему указано имя, создает «именованный слот». Именованный слот можно использовать для настройки содержимого веб-компонента. Элемент <slot> дает нам возможность контролировать, куда должны быть вставлены дочерние элементы пользовательского элемента в его теневое дерево.

В нашем шаблоне мы меняем <legend> на <slot> :

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

Атрибут name используется для назначения слотов другим элементам, если элемент имеет атрибут слота , значение которого соответствует имени именованного слота. Если пользовательский элемент не соответствует слоту, будет отображено содержимое <slot> . Поэтому мы включили <legend> с общим содержимым, которое можно отобразить, если кто-то просто включит <star-rating></star-rating> без содержания в свой HTML.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

Атрибут slot — это глобальный атрибут, который используется для замены содержимого <slot> внутри <template> . В нашем пользовательском элементе элементом с атрибутом slot является <legend> . Это не обязательно. В нашем шаблоне <slot name="star-rating-legend"> будет заменен на <anyElement slot="star-rating-legend"> , где <anyElement> может быть любым элементом, даже другим пользовательским элементом.

Неопределенные элементы

В нашем <template> мы использовали элемент <rating> . Это не пользовательский элемент. Скорее, это неизвестный элемент. Браузеры не выходят из строя, если не распознают элемент. Нераспознанные элементы HTML обрабатываются браузером как анонимные встроенные элементы, которым можно стилизовать с помощью CSS. Подобно <span> , элементы <rating> и <star-rating> не имеют стилей или семантики, применяемых пользовательским агентом.

Обратите внимание, что <template> и его содержимое не отображаются. <template> — это известный элемент, содержащий контент, который не подлежит отображению. Элемент <star-rating> еще не определен. Пока мы не определим элемент, браузер отображает его как все нераспознанные элементы. На данный момент нераспознанный <star-rating> рассматривается как анонимный встроенный элемент, поэтому содержимое, включая легенды и <p> в третьем <star-rating> , отображается так, как если бы они находились в <span> вместо.

Давайте определим наш элемент, чтобы преобразовать этот нераспознанный элемент в пользовательский элемент.

Пользовательские элементы

Для определения пользовательских элементов требуется JavaScript. После определения содержимое элемента <star-rating> будет заменено теневым корнем, содержащим все содержимое шаблона, который мы с ним связываем. Элементы <slot> из шаблона заменяются содержимым элемента в <star-rating> , значение атрибута slot которого соответствует значению имени <slot> , если таковое имеется. Если нет, отображается содержимое слотов шаблона.

Содержимое пользовательского элемента, который не связан со слотом — <p>Is this text visible?</p> в нашем третьем <star-rating> — не включен в теневой корень и, следовательно, не отображается.

Мы определяем пользовательский элемент с именем star-rating , расширяя HTMLElement :

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Теперь, когда элемент определен, каждый раз, когда браузер встречает элемент <star-rating> , он будет отображаться так, как определено элементом с #star-rating-template , который является нашим шаблоном. Браузер прикрепит теневое дерево DOM к узлу, добавив клон содержимого шаблона в этот теневой DOM. Обратите внимание, что количество элементов, к которым вы можете attachShadow() ограничено .

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Если вы посмотрите на инструменты разработчика, вы заметите, что <form> из <template> является частью теневого корня каждого пользовательского элемента. Клон содержимого <template> виден в каждом настраиваемом элементе в инструментах разработчика и виден в браузере, но содержимое самого настраиваемого элемента не отображается на экране.

Снимок экрана DevTools, показывающий клонированное содержимое шаблона в каждом настраиваемом элементе.

В примере <template> мы добавили содержимое шаблона в тело документа, добавив содержимое в обычный DOM. В определении customElements мы использовали тот же appendChild() , но содержимое клонированного шаблона добавлялось в инкапсулированный теневой DOM.

Обратите внимание, как звезды снова стали нестилизованными переключателями? Поскольку это часть теневого DOM, а не стандартного DOM, стиль на вкладке CSS Codepen не применяется. Стили CSS этой вкладки привязаны к документу, а не к теневой модели DOM, поэтому стили не применяются. Нам нужно создать инкапсулированные стили для стилизации нашего инкапсулированного содержимого Shadow DOM.

Тень ДОМ

Shadow DOM ограничивает стили CSS каждым теневым деревом, изолируя его от остальной части документа. Это означает, что внешний CSS не применяется к вашему компоненту, а стили компонента не влияют на остальную часть документа, если только мы намеренно не направляем их.

Поскольку мы добавили содержимое в теневой DOM, мы можем включить элемент <style> , предоставляющий инкапсулированный CSS в пользовательский элемент.

Поскольку область действия ограничена пользовательским элементом, нам не нужно беспокоиться о том, что стили проникнут в остальную часть документа. Мы можем существенно снизить специфичность селекторов. Например, поскольку в пользовательском элементе используются только переключатели, мы можем использовать input вместо input[type="radio"] в качестве селектора.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Хотя веб-компоненты инкапсулированы с помощью разметки <template> , а стили CSS ограничены теневой DOM и скрыты от всего, что находится за пределами компонентов, содержимое слота, которое визуализируется, <anyElement slot="star-rating-legend"> часть <star-rating> не инкапсулирована.

Стилизация за пределами текущей области действия

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

Теневое дерево — это дерево DOM внутри теневого DOM. Теневой корень — это корневой узел теневого дерева.

Псевдокласс :host выбирает <star-rating> , элемент теневого хоста. Теневой хост — это узел DOM, к которому подключен теневой DOM. Чтобы настроить таргетинг только на определенные версии хоста, используйте :host() . При этом будут выбраны только те элементы теневого хоста, которые соответствуют переданному параметру, например, селектор класса или атрибута. Чтобы выбрать все пользовательские элементы, вы можете использовать star-rating { /* styles */ } в глобальном CSS или :host(:not(#nonExistantId)) в стилях шаблона. По специфичности выигрывает глобальный CSS.

Псевдоэлемент ::slotted() пересекает границу теневого DOM изнутри теневого DOM. Он выбирает элемент с прорезями, если он соответствует селектору. В нашем примере ::slotted(legend) соответствует нашим трем легендам.

Чтобы настроить теневой DOM из CSS в глобальной области видимости, необходимо отредактировать шаблон. Атрибут part можно добавить к любому элементу, который вы хотите стилизовать. Затем используйте псевдоэлемент ::part() для сопоставления элементов теневого дерева, соответствующих переданному параметру. Привязкой или исходным элементом для псевдоэлемента является хост или имя пользовательского элемента, в данном случае star-rating . Параметр представляет собой значение атрибута part .

Если бы наша разметка шаблона начиналась так:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Мы могли бы настроить таргетинг на <form> и <fieldset> с помощью:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Имена частей действуют аналогично классам: элемент может иметь несколько имен частей, разделенных пробелами, и несколько элементов могут иметь одно и то же имя части.

У Google есть фантастический контрольный список для создания пользовательских элементов . Вы также можете узнать о декларативных теневых DOM .

Проверьте свое понимание

Проверьте свои знания о шаблоне, слоте и тени.

По умолчанию стили снаружи теневого DOM будут стилизовать элементы внутри.

Истинный.
ЛОЖЬ.

Какой ответ является правильным описанием элемента <template> ?

Элемент-заполнитель.
Элемент, используемый для объявления фрагментов HTML, которые не будут отображаться по умолчанию.
Общий элемент, используемый для отображения любого контента на вашей странице.