Предлагаю вашему вниманию перевод статьи «Introducing Slot-Based Shadow DOM API» автора Ryosuke Niwa, написанную им в блоге WebKit осенью прошлого года.
Мы рады анонсировать что базовая поддержка нового Shadow DOM API на основе слотов, которую мы предлагали в апреле (прим. переводчика: речь идёт об апреле 2015) уже доступна в ночных сборках WebKit после r190680. Shadow DOM это часть Веб Компонентов – набора спецификаций, изначально предложенных Google для того чтобы сделать возможным создание переиспользуемых виджетов и компонентов в вебе. Shadow DOM, в частности, предоставляет легковесную инкапсуляцию DOM дерева, позволяя создавать на элементе параллельное дерево, так называемое «теневое shadow дерево», с помощью которого изменяется отрисовка элемента без изменения DOM. Пользователи такого компонента не смогут ненароком что-то в нём изменить, ведь его shadow дерево не является привычным потомком элемента-хоста. Кроме того, действие стилей также ограничено областью действия (scope), а значит CSS правила, объявленные снаружи shadow дерева не применяются к элементам внутри такого дерева, а правила, объявленные внутри – к элементам снаружи.

С такой реализацией прогресбара есть одна проблема: оба его div'а доступны любому желающему, а стили не ограничены только рамками самого элемента. К примеру, стили прогресбара, определённые для CSS класса
Совет: во время дебага может оказаться полезным режим

Вместо того чтобы копировать весь этот текст в наш собственный shadow DOM, мы могли бы следующим образом использовать именованные слоты для отрисовки текста в коде нашего shadow DOM не меняя его:
Мы рады анонсировать что базовая поддержка нового Shadow DOM API на основе слотов, которую мы предлагали в апреле (прим. переводчика: речь идёт об апреле 2015) уже доступна в ночных сборках WebKit после r190680. Shadow DOM это часть Веб Компонентов – набора спецификаций, изначально предложенных Google для того чтобы сделать возможным создание переиспользуемых виджетов и компонентов в вебе. Shadow DOM, в частности, предоставляет легковесную инкапсуляцию DOM дерева, позволяя создавать на элементе параллельное дерево, так называемое «теневое shadow дерево», с помощью которого изменяется отрисовка элемента без изменения DOM. Пользователи такого компонента не смогут ненароком что-то в нём изменить, ведь его shadow дерево не является привычным потомком элемента-хоста. Кроме того, действие стилей также ограничено областью действия (scope), а значит CSS правила, объявленные снаружи shadow дерева не применяются к элементам внутри такого дерева, а правила, объявленные внутри – к элементам снаружи.
Изоляция стилей
Первое значительное преимущество использования shadow DOM – это изоляция стилей. Представим, что мы хотим создать собственный прогресбар. Например, следующим образом мы могли бы использовать два вложенных div'а для того чтобы представить сам прогресбар и ещё один div с текстом, в котором показывать процент выполнения:Обратите внимание на элемент template, использование которого позволяет автору включить сниппет HTML-текста, чтобы позже быть инстанциированным путём создания клона. Это первая фича «веб компонентов», внедрённая нами в WebKit; позже её включили в спецификацию HTML5. Элементу template в документе разрешено появляться в любом месте (скажем, между<style> .progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; } .progress > .bar { background: #9cf; height: 100%; } .progress > .label { position: absolute; top: 0; left: 0; width: 100%; text-align: center; font-size: 0.8rem; line-height: 1.1rem; } </style> <template id="progress-bar-template"> <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> <div class="bar"></div> <div class="label">0%</div> </div> </template> <script> function createProgressBar() { var fragment = document.getElementById('progress-bar-template').content.cloneNode(true); var progressBar = fragment.querySelector('div'); progressBar.updateProgress = function (newPercentage) { this.setAttribute('aria-valuenow', newPercentage); this.querySelector('.label').textContent = newPercentage + '%'; this.querySelector('.bar').style.width = newPercentage + '%'; } return progressBar; } </script>
table и tr), а содержимое внутри template инертно и не выполняет скриптов и загрузку изображений или любых других ресурсов. Таким образом, пользователю сего прогресбара будет достаточно инстанциировать и обновлять его как показано ниже:var progressBar = createProgressBar(); container.appendChild(progressBar); ... progressBar.updateProgress(10);

С такой реализацией прогресбара есть одна проблема: оба его div'а доступны любому желающему, а стили не ограничены только рамками самого элемента. К примеру, стили прогресбара, определённые для CSS класса
progress будут так же применены и к следующему HTML:А стили других элементов будут переопределять внешний вид прогресбара:<section class="project"> <p class="progress">Pending an approval</p> </section>
Мы могли бы обойти эти ограничения, дав прогресбару имя custom element, например<style> .label { font-weight: bold; } </style>
custom-progressbar чтобы ограничить область действия стилей, а затем проинициализировать все остальные свойства в all: initial, однако в мире Shadow DOM есть более элегантное решение. Основная идея в том, чтобы представить внешний div в качестве дополнительного слоя инкапсуляции так что пользователи не увидят что происходит внутри (создание div'ов для лейбы и самого ползунка), стили прогресбара не будут вмешиваться в работу остальной страницы и наоборот. Для этого нам понадобится сначала создать ShadowRoot, вызвав метод attachShadow({mode: 'closed'}) у прогресбара, а следом вставить в него DOM узлы, необходимые для нашей реализации. Допустим, мы и дальше используем div для задания хоста данному shadow root, тогда мы можем следующим образом создать новый div и приаттачить shadow root:Обратите внимание, что элемент style находится внутри template и будет склонирован в shadow root вместе с div'ами. Это ограничит область действия стилей этим самым shadow root. Точно так же стили снаружи не применяются к элементам внутри.<template id="progress-bar-template"> <style> .progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; } .progress > .bar { background: #9cf; height: 100%; } .progress > .label { position: absolute; top: 0; left: 0; width: 100%; text-align: center; font-size: 0.8rem; line-height: 1.1rem; } </style> <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> <div class="bar"></div> <div class="label">0%</div> </div> </template> <script> function createProgressBar() { var progressBar = document.createElement('div'); var shadowRoot = progressBar.attachShadow({mode: 'closed'}); shadowRoot.appendChild(document.getElementById('progress-bar-template').content.cloneNode(true)); progressBar.updateProgress = function (newPercentage) { shadowRoot.querySelector('.progress').setAttribute('aria-valuenow', newPercentage); shadowRoot.querySelector('.label').textContent = newPercentage + '%'; shadowRoot.querySelector('.bar').style.width = newPercentage + '%'; } return progressBar; } </script>
Совет: во время дебага может оказаться полезным режим
open shadow DOM, при котором shadow root будет доступен через свойство shadowRoot элемента-хоста. Например, {mode: DEBUG ? 'open' : 'closed'}Копозиция слотов
Внимательный читатель к этому моменту наверняка задался вопросом: почему бы не сделать это средствами CSS и не лезть в DOM? Стилизация это концепция представления, зачем же мы добавляем новые элементы в DOM? На самом деле, первый публичный рабочий черновик CSS Scoping Module Level 1 определяет правило @scope как раз для этих целей. Зачем же по��адобился ещё один механизм изолирования стилей? Хорошей причиной для имплементации послужило то, что элементы реализованные внутри компонента скрыты от внешних механизмов обхода узлов, таких какquerySelectorAll и getElementsByTagName. Из-за того что по умолчанию узлы внутри shadow root не обнаруживаются этими API, пользователи компонент могут не задумываться о внутренней реализации каждого компонента. Каждый компонент представлен в виде непрозрачного элемента, детали реализации которого инкапсулированы внутри его shadow DOM. Имейте в виду, что shadow DOM никоим образом не заботится о cross-origin ограничениях как это делает элемент iframe. При необходимости другие сценарии смогут проникнуть внутрь shadow DOM. Однако, есть и другая причина по которой появился этот механизм – композиция. Допустим, у нас есть список контактов:и хотим мы каждому пункту контактной информации из списка добавить красивостей при включённых скриптах:<ul id="contacts"> <li> Commit Queue (<a href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br> One Infinite Loop, Cupertino, CA 95014 </li> <li> Niwa, Ryosuke (<a href="mailto:rniwa@webkit.org">rniwa@webkit.org</a>)<br> Two Infinite Loop, Cupertino, CA 95014 </li> </ul>

Вместо того чтобы копировать весь этот текст в наш собственный shadow DOM, мы могли бы следующим образом использовать именованные слоты для отрисовки текста в коде нашего shadow DOM не меняя его:
Концептуально слоты – это незаполненные пробелы в shadow DOM, заполняемые потомками элемента-хоста. Каждый элемент назначается слоту с именем, определённым в атрибуте<template id="contact-template"> <style> :host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; } b { display: inline-block; width: 5rem; } </style> <b>Name</b>: <slot name="fullName"><slot name="firstName"></slot> <slot name="lastName"></slot></slot><br> <b>Email</b>: <slot name="email">Unknown</slot><br> <b>Address</b>: <slot name="address">Unknown</slot> </template> <script> window.addEventListener('DOMContentLoaded', function () { var contacts = document.getElementById('contacts').children; var template = document.getElementById('contact-template').content; for (var i = 0; i < contacts.length; i++) contacts[i].attachShadow({mode: 'closed'}).appendChild(template.cloneNode(true)); }); </script>
slot:Таким образом мы присоединяем к<ul id="contacts"> <li> <span slot="fullName">Commit Queue</span> (<a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br> <span slot="address">One Infinite Loop, Cupertino, CA 95014</span> </li> </ul>
li наш shadow root, а каждый span с атрибутом slot назначается слоту с соответствующим именем внутри shadow DOM. Взглянем подробнее на шаблон shadow DOM:В этом шаблоне имеется два слота с названиями<b>Name</b>: <slot name="fullName"> <slot name="firstName"></slot> <slot name="lastName"></slot> </slot><br> <b>Email</b>: <slot name="email">Unknown</slot><br> <b>Address</b>: <slot name="address">Unknown</slot>
email и address, а также слот с названием fullName, содержащий внутри себя два других слота firstName и lastName. Слот fullName пользуется техникой фолбека, когда firstName и lastName отображаются только в случае отсутствия узлов назначенных fullName. Несмотря на то, что в данном случае каждому слоту назначен ровно один узел, мы могли бы назначить множество элементов с одинаковым атрибутом slot одному и тому же слоту, тогда бы они отображались в том же порядке, в каком они расположены потомками хост-элемента. Можно также использовать безымянные стандартные слоты, их заполнят те потомки хоста, у которых не указан атрибут slot. Когда браузер рендерит этот компонент, содержимое li заменяется на shadow DOM, а слоты внутри него заменяются назначенными узлами так, будто на самом деле отображается следующий DOM:Как видите, основанная на слотах композиция это мощный инструмент, позволяющий виджетам вставлять в страницу содержимое без клонирования и изменения DOM. С его помощью виджеты могут реагировать на изменения их потомков не прибегая к MutationObserver или каким-либо явным уведомлениям от сценариев. В сущности, композиция превращает DOM в связующий механизм коммуникации между компонентами.<ul id="contacts"> <li> <!--shadow-root-start--> <b>Name</b>: <slot name="fullName"> <!--slot-content-start--> <span slot="fullName">Commit Queue</span> <!--slot-content-end--> </slot><br> <b>Email</b>: <slot name="email"> <!--slot-content-start--> <a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a> <!--slot-content-end--> </slot><br> <b>Address</b>: <slot name="address"> <!--slot-content-start--> <span slot="address">One Infinite Loop, Cupertino, CA 95014</span> <!--slot-content-end--> </slot> <!--shadow-root-end--> </li> </ul>
Стилизация хост-элемента
Есть ещё один момент, который стоит отметить в предыдущем примере – мистический псевдокласс:host:Этот псевдокласс, как следует из его имени, применяется к хосту shadow DOM, в котором находится это правило. По умолчанию, авторские стили снаружи shadow DOM имеют более высокий приоритет в сравнении со стилями внутри shadow DOM. Это сделано чтобы внутри компонента можно было определить «стили по умолчанию», а пользователям компонента дать возможность их переопределять когда нужно. В дополнение, компонент может определить принципиально важные для его отображения стили (такие, например, как ширина или<template id="contact-template"> <style> :host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; } b { display: inline-block; width: 5rem; } </style> ... </template>
display) с ключевым словом !important. Любые !important правила внутри shadow DOM считаются более приоритетными тех !important, что объявлены снаружи.