Дозвольте представити, Shadow DOM API на основі слотів

Пропоную вашій увазі переклад статті «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 дерева не застосовуються до елементів всередині такого дерева, а правила, оголошені всередині – до елементів зовні.

Ізоляція стилів

Перша значна перевага використання shadow DOM – це ізоляція стилів. Уявімо, що ми хочемо створити власний прогресбар. Наприклад, таким чином ми могли б використовувати два вкладених div а для того щоб уявити сам прогресбар і ще один div з текстом, в якому показувати відсоток виконання:
<style>
.progress { position: relative; border: 1px solid #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.8 rem; line-height: 1.1 rem; }
</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: 1px solid #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.8 rem; line-height: 1.1 rem; }
</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: 1px solid #ccc; border-radius: 0.5 rem; padding: 0.5 rem; margin: 0.5 rem; }
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: 1px solid #ccc; border-radius: 0.5 rem; padding: 0.5 rem; margin: 0.5 rem; }
b { display: inline-block; width: 5rem; }
</style>
...
</template>
Цей псевдоклас, як випливає з його імені, застосовується до хосту shadow DOM, в якому знаходиться це правило. За замовчуванням, авторські стилі зовні shadow DOM мають більш високий пріоритет у порівнянні зі стилями всередині shadow DOM. Це зроблено щоб усередині компонента можна було визначити «стилі за замовчуванням», а користувачам компонента дати можливість їх змінити. На додаток, компонент може визначити принципово важливі для його відображення стилі (такі, наприклад, як ширина або
display
) з ключовим словом
!important
. Будь-які
!important
правила всередині shadow DOM вважаються більш пріоритетними тих
!important
, що оголошені зовні.

Подальша робота

Багато роботи ще попереду щодо впровадження Веб Компонентів. Ми б хотіли дозволити стилізувати слоти shadow DOM через стилі відповідних зовнішніх вузлів. Є також побажання навчити компоненти вбудовуватися в тему документа, а також виставляти назовні стилизируемые частини у вигляді псевдоелемент CSS. У довгостроковій перспективі ми б хотіли побачити імперативний DOM API для маніпуляції призначенням слотів, ми вже давно пропонували це зробити. А ще ми зацікавлені у додатку shadow DOM довільними (custom) елементами. Коротенько, custom elements API дає авторам можливість асоціювати класи JavaScript з конкретним ім'ям елемента у HTML документах; відмінний спосіб идеоматически призначати довільна поведінка і shadow DOM. На жаль, на даний момент існує несколько противоречивых предложений про те як і коли створювати довільні елементи. Щоб допомогти направити обговорення W3C ми плануємо зробити прототип WebKit. Для складання пакетів і постачання Веб Компонентів ми працюємо на модулями ES6. Як і Mozilla, ми віримо що модулі радикально змінять ставлення авторів до структурування їх сторінок. В кінцевому рахунку ми хотіли б спроектувати API створення повністю ізольованих веб компонент з подібними iframe політиками безпеки, заснованими на shadow DOM і довільних елементах. На закінчення, хотілося б зазначити, що ми дійсно дуже пишаємося тим, що значні можливості Веб Компонентів з'являються в WebKit, ми будемо і далі писати про нові з'являються можливості. У випадку якщо у вас залишилися якісь питання-звертайтеся безпосередньо до мене, @WebKit або Джону Девісу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.