Предлагаю вашему вниманию перевод статьи «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 с текстом, в котором показывать процент выполнения:<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>
Обратите внимание на элемент template, использование которого позволяет автору включить сниппет HTML-текста, чтобы позже быть инстанциированным путём создания клона. Это первая фича «веб компонентов», внедрённая нами в WebKit; позже её включили в спецификацию HTML5. Элементу template в документе разрешено появляться в любом месте (скажем, между 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>
А стили других элементов будут переопределять внешний вид прогресбара:<style>
.label { font-weight: bold; }
</style>
Мы могли бы обойти эти ограничения, дав прогресбару имя custom element, например custom-progressbar ��тобы ограничить область действия стилей, а затем проинициализировать все остальные свойства в all: initial, однако в мире Shadow DOM есть более элегантное решение. Основная идея в том, чтобы представить внешний div в качестве дополнительного слоя инкапсуляции так что пользователи не увидят что происходит внутри (создание div'ов для лейбы и самого ползунка), стили прогресбара не будут вмешиваться в работу остальной страницы и наоборот. Для этого нам понадобится сначала создать ShadowRoot, вызвав метод attachShadow({mode: 'closed'}) у прогресбара, а следом вставить в него DOM узлы, необходимые для нашей реализации. Допустим, мы и дальше используем div для задания хоста данному 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>
Обратите внимание, что элемент style находится внутри template и будет склонирован в shadow root вместе с div'ами. Это ограничит область действия стилей этим самым shadow root. Точно так же стили снаружи не применяются к элементам внутри.Совет: во время дебага может оказаться полезным режим
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 не меняя его:
<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>
Концептуально слоты – это незаполненные пробелы в shadow DOM, заполняемые потомками элемента-хоста. Каждый элемент назначается слоту с именем, определённым в атрибуте 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:<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>
Как видите, основанная на слотах композиция это мощный инструмент, позволяющий виджетам вставлять в страницу содержимое без клонирования и изменения DOM. С его помощью виджеты могут реагировать на изменения их потомков не прибегая к MutationObserver или каким-либо явным уведомлениям от сценариев. В сущности, композиция превращает DOM в связующий механизм коммуникации между компонентами.Стилизация хост-элемента
Есть ещё один момент, который стоит отметить в предыдущем примере – мистический псевдокласс:host:<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>
Этот псевдокласс, как следует из его имени, применяется к хосту shadow DOM, в котором находится это правило. По умолчанию, авторские стили снаружи shadow DOM имеют более высокий приоритет в сравнении со стилями внутри shadow DOM. Это сделано чтобы внутри компонента можно было определить «стили по умолчанию», а пользователям компонента дать возможность их переопределять когда нужно. В дополнение, компонент может определить принципиально важные для его отображения стили (такие, например, как ширина или display) с ключевым словом !important. Любые !important правила внутри shadow DOM считаются более приоритетными тех !important, что объявлены снаружи.