Первая часть обзора веб-компонентов посвящена стандарту Custom Elements.

Теневой DOM

Спецификации Shadow DOM привносят концепцию области в определения веб-стилей. Эти области реализуются через несколько DOM деревьев, встроенных в один документ. В некотором смысле подход Shadow DOM можно сравнить с элементом iframe внутри документа, поскольку он имеет естественно изолированную структуру DOM. Что касается документов, определения Shadow DOM распределены между несколькими различными стандартами, такими как DOM specification, HTML specification, CSS Scoping Module, UI Events и т. Д.

DOM specification определяет модель для деревьев узлов и то, как узлы взаимодействуют друг с другом посредством событий. Дерево узлов DOM - это иерархическая структура узлов, в которой одни узлы могут разветвляться, а другие - просто листья. Спецификация различает Document Tree и Shadow Tree через подключение к их корням. Объект document является корнем Document Tree, в то время как для Shadow Tree предлагается специальный тип корня, который называется Shadow Root. Shadow Root должен быть прикреплен к любому другому дереву под элементом host. Shadow Root можно рассматривать как Document Fragment - спецификация определяет отношения принадлежности между двумя интерфейсами.

<script>
 class CustomElement extends HTMLElement {
   constructor() {
     super()
     this.attachShadow({ mode: 'open' })
   }
   connectedCallback() {
     this.shadowRoot.innerHTML = '<a href="#">My link</a>'
   }
 }
 customElements.define('hello-shadow-dom', CustomElement)
</script>
<hello-shadow-dom></hello-shadow-dom>

Единственный способ создать Shadow DOM - вызвать метод attachShadow() класса Element. Он создает теневой корень для элемента. К элементу можно прикрепить только один теневой корень. Свойство mode, указанное в attachShadow() call, определяет, доступно ли дерево из JavaScript или нет. Альтернативой «открытому» значению может быть «закрытое» значение. Присоединенное свойство shadowRoot является родительским для указанного дерева узлов. shadowRoot.host настроен на элемент hello-shadow-dom в следующем примере.

<hello-shadow-dom></hello-shadow-dom>
 #shadow-root (open)
   <a href="#">My link</a>

Функция Slot широко используется в UI Libraries. В Angular, это называется проекцией или включением и поддерживается директивой ng-content. Vue кажется более совместимым со стандартами - он вызывает те же функции, что и slot. Наконец, в React эта способность еще более актуальна для библиотеки и может быть достигнута {props.children} использованием внутри контейнера элементов. Функция Slot служит заполнителю. С одной стороны, Shadow Tree может содержать slots элементы, определенные с помощью тега slot. С другой стороны, когда разработчик хочет использовать элемент, он или она может указать разметку для объявленного slots, чтобы содержимое отображалось внутри Shadow Tree оболочки.

<script>
 class OhMySlotElement extends HTMLElement {
   constructor() {
     super()
     this.attachShadow({ mode: 'open' })
   }
   connectedCallback() {
     this.shadowRoot.innerHTML = `<p>
       <slot name="my-text">default</slot>
     </p>`
   }
 }
 customElements.define('oh-my-slot', OhMySlotElement);
</script>
<oh-my-slot>
 <pre slot="my-text">
   anything
 </pre>
</oh-my-slot>

Последнее дерево DOM содержит как Shadow Root с дочерним элементом p, так и элемент pre разработчика. Последний связан с узлом slot и заменяет его содержимое по умолчанию. Атрибут name в теге slot определяет, какой именно заполнитель использовать. Также можно использовать безымянный слот, если нет доступных альтернатив.

Спецификация Shadow DOM позволяет разработчику объявлять изолированные стили и предоставляет API для использования основного контекста документа. Элементы внутри Shadow Tree недоступны для внешних селекторов CSS документа.

В следующем примере представлены два раздела стилей: один находится внутри основного документа, другой - внутри прикрепленного элемента Shadow DOM.

<style>h3 { color: blue; }</style>
<script>
 class CustomElement extends HTMLElement {
   constructor() {
     super()
     this.attachShadow({ mode: 'open' })
   }
   connectedCallback() {
     this.shadowRoot.innerHTML = `
       <style>h3 { color: red; }</style>
       <h3>Shadow DOM</h3>
     `
   }
 }
 customElements.define('my-element', CustomElement);
</script>
<h3>Normal DOM</h3>
<my-element></my-element>

Стиль, примененный к элементу h3 основного документа, имеет синий цвет, а h3 внутри my-element окрашен в красный цвет. Примечательно, что основной документ может определять стили для самого host элемента. Если для стиля my-element задан зеленый цвет, он будет применен к его дочерним элементам, как в обычной ситуации наследования стилей. Унаследованный цвет по-прежнему имеет меньший приоритет, чем любой другой определенный стиль.

Селектор псевдокласса :host смотрит в элемент host Shadow Root.

:host { color: green; }

Стили в основном документе имеют больший приоритет по сравнению со стилями :host внутри элемента. Можно использовать функцию :host() для условных селекторов

:host(.error) { color: red; }

Когда к host присоединен класс error, к нему будет применен красный цвет.

Другой функциональный селектор - :host-context(), который может определить, есть ли предок, соответствующий указанному селектору за пределами Shadow Tree.

Точно так же функция ::slotted() может применяться к «внешним» элементам внутри slots.

Еще есть возможность настраивать стили в основном документе. Это функция CSS custom properties или переменных, примененная к Shadow DOM. Он основан на нескольких принципах:

  • Свойства определены и используются внутри Shadow DOM. Таким образом, автор компонента несет ответственность за предоставление API (или списка свойств, которые могут быть перезаписаны извне).
  • Переменные применяются к элементу host в основном документе. Как сказано в спецификации, настраиваемые свойства наследуются. Это означает, что если он не определен на уровне дочернего элемента, он принимает значение родительского элемента.

Когда пользовательский элемент использует Shadow DOM со стилем, определенным как:

h3 { color: var(--my-color); }

Он попытается получить доступ к переменной --my-color. Таким образом, разработчик, использующий элемент, может добавить переменную в свои определения стиля.

custom-element { --my-color: red; }

Однако переменная --my-color не определена в селекторе h3 ни в одном определении стиля, она наследуется от host Shadow Root и применяется к DOM внутреннего элемента. Значение цвета h3 красный.

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

Подводя итог, Shadow DOM предлагает следующие функции:

  • Изолированный DOM, отделенный от документа.
  • Составление элемента с частным или публичным доступом к дереву.
  • slots с целью повторного использования.
  • CSS имеет ограниченную область видимости, поэтому стили страницы не смешиваются со стилями, объявленными внутри Shadow Tree. Такой подход упрощает определение стилей и снижает потребность в иерархических селекторах.

HTML-шаблон

Спецификация HTML Template обычно сочетается с Shadow DOM и Custom Elements. В нем объясняется, как указать макет, не влияя на сам документ. Любое содержимое внутри тега template не отображается при загрузке страницы и может использоваться позже кодом JavaScript для создания экземпляров элементов.

<template id="mytemplate">
 <script>
   alert()
 </script>
</template>

В данном примере браузер теперь покажет всплывающее окно; однако это указано внутри тега script.

const template = document.querySelector('#mytemplate')
const clone = document.importNode(template.content, true)
document.body.appendChild(clone)

Всплывающее окно появляется только тогда, когда шаблон создан и добавлен в документ. content - это еще один Document Fragment, который содержит указанную структуру HTML. Метод документа importNode() генерирует копию данного узла. Последний аргумент, который он принимает, - это «глубокий» флаг. По умолчанию это false, и будет скопирован только элемент высокого уровня, поэтому для копирования всего макета следует использовать true.

Имеет смысл создать template элементов, содержащих внутри style определение.

<template id="mytemplate">
 <style>
   :host { color: red }
 </style>
</template>

Определение соответствует естественному синтаксису HTML, не отображается при загрузке страницы и может использоваться позже, когда разработчику необходимо добавить экземпляр шаблона.

const template = document.querySelector('#mytemplate')
const div = document.createElement('div')
const root = div.attachShadow({ mode: 'open' })
const clone = document.importNode(template.content, true)
root.appendChild(clone)
document.body.appendChild(div)

Резюме

Спецификации Custom Elements, Shadow DOM и HTML Template позволяют разработчику создавать повторно используемые компоненты с инкапсулированными DOM стилями и поведением. Большинство заявленных API уже доступны в основных браузерах и имеют полифилы, имитирующие остальные.

Удачного кодирования и надеемся увидеть вас в комментариях!