Доклад на SECON’2014

Введение

Всем известно что в конце 2012 года W3C представила рабочую версию стандарта HTML5.
Но осталось незамеченным другое знаковое событие - в Chrome v.25 (02.2013) были имплементированы пилотные варианты спецификаций W3C Web Components, работа над которыми ведется с 2008 года.
Web Components - одно из направлений деятельности 1 группы W3C Web Applications (WEBAPPS) Working Group 2.

Основные спецификации этого направления:

Комплекс спецификаций Web Components:

  • позволяет разработчикам web-приложений нативными средствами браузера создавать сложные компоненты пользовательского интерфейса с возможностью наследования и повторного использования;
  • предоставляет средства описания шаблона компонента, его поведения, стилей отображения, а также методы их инкапсуляции в единую независимую сущность.

Web Components является логичным развитием возможностей браузеров, которое произошло под влиянием массы UI-фреймворков - Prototype JavaScript Framework, The Yahoo! User Interface Library, Ext JS, JQuery, jQuery UI, Twitter Bootstrap.

Shadow DOM

Назначение

Технология Shadow DOM:

  • включает в себя механизмы объявления и использования независимых поддеревьев в основном DOM-дереве документа;
  • предоставляет возможность инкапсулировать структуру компонента (HTML), стили его отображения (CSS) и описание его поведения (JavaScript).

В совокупности с другими технологиями стека Web Components это позволяет создавать компоненты со сложной внутренней организацией и прозрачным внешним API.
По-сути, Shadow DOM связывает в единое целое возможности технологий стека Web Components (HTML Imports, Custom Elements, HTML Templates)

Мы используем Shadow DOM?

Ответ - Да. Технологии Web Components уже используются для отрисовки нативных UI-компонент.
Рассмотрим использование Shadow DOM на примере некоторых нативных тегов.

Внимание!
Для просмотра примеров используйте Chrome Canary с включенными флагами:

  • Experimental Web Platform features (Включить экспериментальные функции веб-платформы)
    chrome://flags/#enable-experimental-web-platform-features
  • Enable HTML Imports (Разрешить импорт HTML-файлов)
    chrome://flags/#enable-html-imports
  • Experimental JavaScript (Включить экспериментальный JavaScript)
    chrome://flags/#enable-javascript-harmony

Для отображения Shadow DOM-элементов в отладчике необходимо настроить Chrome Developer Tools:

  • откройте Chrome Developer Tools - F12
  • откройте панель настроек -
  • на вкладке “General” включите “Show Shadow DOM”.
<video id="video" controls="" preload="none" poster="http://media.w3.org/2010/05/sintel/poster.png">
    <source id="mp4" src="http://media.w3.org/2010/05/sintel/trailer.mp4" type="video/mp4">
    <source id="webm" src="http://media.w3.org/2010/05/sintel/trailer.webm" type="video/webm">
    <source id="ogv" src="http://media.w3.org/2010/05/sintel/trailer.ogv" type="video/ogg">
</video>
<dl class="dl-horizontal">
    <dt>Input Date:</dt>      <dd><input type="date"></dd>
    <dt>Input Time:</dt>      <dd><input type="time"></dd>
    <dt>Input DateTime:</dt>  <dd><input type="datetime-local"></dd>
    <dt>Input File:</dt>      <dd><input type="file"></dd>
    <dt>Input Color:</dt>     <dd><input type="color"></dd>
    <dt>Input Range:</dt>     <dd><input type="range" class="inline-block"></dd>
    <dt>Input Text:</dt>      <dd><input type="text"></dd>
    <dt>Input Search:</dt>    <dd><input type="search"></dd>
    <dt>Input Button:</dt>    <dd><input type="button" value="Button"></dd>
    <dt>Textarea:</dt>        <dd><textarea>Foo</textarea></dd>
    <dt>Meter:</dt>           <dd><meter value="0.6"></meter></dd>
    <dt>Progress:</dt>        <dd><progress value="0.6"></progress></dd>
    <dt>Details:</dt>         <dd><details><summary>Summary</summary>
                                           <p>Lots of details here</p></details></dd>
    <dt>Keygen:</dt>          <dd><keygen name="test"></meter></dd>
</dl>
Input Date:
Input Time:
Input DateTime:
Input File:
Input Color:
Input Range:
Input Text:
Input Search:
Input Button:
Textarea:
Meter:
Progress:
Details:
Summary

Lots of details here

Keygen:
</meter>

Просто о главном

  • с элементом основного DOM-дерева может быть ассоциировано Shadow DOM-дерево, этот процесс называется созданием ShadowRoot-дерева для элемента, а сам элемент в данном случае называется host-элементом;
  • элементы ShadowRoot-дерева не являются дочерними элементами host-элемента, скрыты при выборе селекторами из основного дерева, т.е. ShadowRoot-дерево выступает в качестве независимого DOM-дерева со своей областью видимости (может содержать одноименные с основным документом ID элементов или селекторы в стилях, не оказывая при этом влияния на основное дерево)

Исследуем возможности Shadow DOM на примере.

<button class="bws">
    <u>Get time...</u>
</button>
<script>(function(){
    var host = document.querySelector('.bws'); // находим host-элемент
    host.onclick = function(){
        var root = host.createShadowRoot();    // создаем ShadowRoot-деревo
        alert('pause');
        root.textContent = (new Date()).toLocaleTimeString(); // наполняем контентом
    };
})();</script>
  • в терминологии Web Components нода, для которой создается ShadowRoot, называется host-нодой (в примере ссылка на эту ноду сохранена в переменной host)
  • после однократного нажатия кнопки происходит создание ShadowRoot, при этом происходит изменение отображения host-ноды - вместо изначального контента host-ноды (“Get time…”) выводится ShadowRoot
  • следующее нажатие кнопки создает еще один ShadowRoot для той же ноды (host-ноды), после чего отображается контент самого свежего ShadowRoot

  • ссылка на текущий (самый свежий) ShadowRoot host-ноды содержится в host.shadowRoot и, соответственно, в ShadowRoot есть ссылка на host-ноду root.host
  • кроме того, в более свежем ShadowRoot есть ссылка на предыдущий root.olderShadowRoot (если таковой существует)

Механизм распределения (Distribution) и точки вставки (Insertion points)

Описание

В предыдущем примере при создании ShadowRoot теряется контент host-ноды. И это естественно - изначальный контент host-ноды является частью DOM дерева основного документа, а при создании ShadowRoot для этой ноды мы определяем для нее “персональное” дерево, контент которого замещает изначальный. Кроме того, host-нода отображает только самый свежий ShadowRoot, хотя их может быть создано несколько. И здесь все логично - создавая новый ShadowRoot host-ноды мы создаем для нее новое DOM-дерево, которое отображается вместо предыдущего.
Для гибкого управления деревом в Shadow DOM предназначен Distribution механизм (механизм распределения). Суть его заключается в использовании точек вставки (insertion points) для обозначении мест вставки контента из одного дерева DOM внутрь другого:

Важно, что точки вставки только указывают на место вставки контента другого дерева, а механизм распределения только отображает в них указанный ими контент не меняя ни одно из этих деревьев.
Ноды, распределенные в Shadow DOM точками вставки, называют distributed nodes. Ими нельзя оперировать посредством обычных селекторов как из дерева основного документа, так и из ShadowRoot-дерева.

Точка <content /> (content insertion point)

Применим content insertion point для отображения контента host-ноды в ее ShadowRoot.

<div id="example-3" markdown="0">
    <button>Get time...</button>
</div>
<script>
(function(){
    var host = document.querySelector('#example-3 > button');
    host.onclick = function(){
       var time = (new Date()).toLocaleTimeString(),
           root = this.createShadowRoot();
       root.innerHTML =
            '<content></content>'+
            '<b>'+time+'</b>';
       };
})();
</script>

Аттрибут select точки <content />

Для insertion point <content /> предусмотрен аттрибут select, значением которого являет список простых селекторов, разделенный запятыми.
Аттрибут предоставляет возможность выбора части контента host-ноды для указания ее места в ShadowRoot.
В предыдущем примере использовалась точка <content></content>, что равнозначно <content select=""></content> или <content select="*"></content> - т.е. происходит выбор всего контента host-ноды.
Применим атибут select для вывода предустановленных значений элемента (не js-хардкодом, а гибко - в верстке).

<div id="example-4" markdown="0">
    <button>
        <div class="title">Get time...</div>
        <div class="time">00:00:00</div>
        <div class="time">01:01:01</div>
    </button>
</div>
<script>
(function(){
    var host = document.querySelector('#example-4 > button'),
        root = host.createShadowRoot();
    root.innerHTML =
            '<content select=".title"></content>'+
            '<b><content select=".time"></content></b>'+
            '<shadow></shadow>';
    host.onclick = function(){
       var time = (new Date()).toLocaleTimeString(),
           r = this.createShadowRoot();
       r.innerHTML =
            '<shadow></shadow>'+
            '<b>'+time+'</b>'+
            '<br />';
       };
})();
</script>

Источники:

Точка <shadow /> (shadow insertion point)

При каждом нажатии кнопки создается новый ShadowRoot. Как упоминалось выше host-нода отображает контент только самого свежего ShadowRoot, хотя их может быть несколько.
Для гибкого использования нескольких ShadowRoot можно определить shadow insertion point с помощью тега <shadow />.

<div id="example-5" markdown="0">
    <button>
        <div class="title">Get time...</div>
        <div class="time">00:00:00</div>
        <div class="time">01:01:01</div>
    </button>
</div>
<script>
(function(){
    var host = document.querySelector('#example-5 > button'),
        root = host.createShadowRoot();
    root.innerHTML =
            '<content select=".title"></content>'+
            '<b><content select=".time"></content></b>'+
            '<shadow></shadow>';
    host.onclick = function(){
       var r = this.createShadowRoot(),
           time = (new Date()).toLocaleTimeString();
        r.innerHTML =
            '<shadow></shadow>'+
            '<b>'+time+'</b>'+
            '<br />';
    };
})();
</script>

Источники:

Доступ к распределенному (distributed) контенту

Как было отмечено выше, распределенный (distributed) посредством insertion points контент скрыт для доступа к нему при помощи селекторов как из основного дерева, так и из ShadowRoot-дерева.
Проверим это утверждение.

document.querySelector('#example-5 > button')
        .shadowRoot
        .querySelectorAll('content *')
        .length; // 0

Получить доступ к распределенному контенту можно вызовом метода .getDistributedNodes() для точек вставки (insertion points).

var points = document.querySelector('#example-5 > button')
                     .shadowRoot
                     .querySelectorAll('content,shadow');
[].slice.call(points)
  .map( function(point){ return [ point,
                                  [].slice.call(point.getDistributedNodes()) ]; } ) );

Доступ к точкам вставки (insertion points)

Для элементов основного и теневого дерева определен метод .getDestinationInsertionPoints(), который возвращает список точек вставки, задействованных для распределения этой ноды.

document.querySelector('#test-5-3 > button')
        .onclick = function(){
            var nodes = document.querySelector('#example-5 > button')
                                .querySelectorAll('.title, .time');
            alert( [].slice.call(nodes)
                     .map( function(node){ return [ node,
                                                    [].slice.call(node.getDestinationInsertionPoints()),
                                                     '\n' ]; } ) );
         };

Источники

HTML Templates

В качестве нативного шаблонизатора на стороне браузера в настоящее время можно рассматривать только XSLT, который используется многими проектами с разной степенью успеха. Сам HTML до последнего времени не предоставлял нативного механизма шаблонизации и для определения шаблонов на стороне браузера обычно используют различные приемы, позволяющие с разной степенью удобства эмулировать механизмы шаблонизации.
Так, в качестве шаблонов могут выступать фрагменты документа, скрытые с помощью css-стилей или строки, заключенные в блоки <script> ... </script>. Каждый из этих приемов имеет свои преимуществаи и недостатки.

W3C в своей спецификации HTML Templates стека Web Components предлагает свой вариант реализации шаблонов на стороне браузера.
Спецификация декларирует элемент HTMLTemplateElement / <template></template>, который позволяет объявлять фрагменты документа в качестве шаблонов.

Особенно примечательны следующие свойства шаблонов:

  • могут быть объявлены внутри <head>, <body> или <frameset>
  • контент шаблона анализируется парсером как HTML
  • контент не активен (скрипты не запускаются, изображения и медиа-контент не загружаются)
  • контент не является частью документа (не отображается, не виден для селекторов)

Конструкции root.innerHTML=" ... <content select='...'></content> ... <shadow></shadow> ... " из предыдущих примеров очень напоминают применение шаблонизатора. Так и есть - в примере намерянно не использовались HTML Templates.

Применим шаблоны в нашем примере.

<div id="example-6" markdown="0">

    <button>
        <div class="title">Get time...</div>
        <div class="time">00:00:00</div>
        <div class="time">01:01:01</div>
    </button>

    <template class="button">
        <content select=".title"></content>
        <b>
           <content select=".time"></content>
        </b>
        <shadow></shadow>
    </template>

    <template class="time">
        <shadow></shadow>
        <b>
            <div class="time"></div>
        </b>
    </template>

</div>
<script>
(function(){
    var example = document.querySelector('#example-6'),
        host = example.querySelector('button'),
        tmpl_button = example.querySelector('template.button'),
        tmpl_time = example.querySelector('template.time'),
        root = host.createShadowRoot();
    root.appendChild( tmpl_button.content.cloneNode(true) );

    host.onclick = function(){

        var r = this.createShadowRoot(),
            time = (new Date()).toLocaleTimeString(),
            $time = tmpl_time.content.cloneNode(true);

        $time.querySelector('.time')
             .textContent = time;

        r.appendChild( $time );
    };
})();
</script>

В примере шаблоны, используемые элементом вынесены из js в html:

  • шаблон для отрисовки элемента при инициализации: <template class="button"> ... </template>
  • шаблон метки времени: <template class="time"> ... </template>

Объект HTMLTemplateElement (tmpl_button, tmpl_time) содержит поле .content, которое хранит контент шаблона в виде объекта DocumentFragment.

Таким образом, наполнение Shadow DOM-дерева можно осуществлять:

  • указанием разметки в поле .innerHTML или текстовых данных в поле .textContent корневого элемента ShadowRoot или его дочерних элементов;
  • созданием дочерних элементов из JS: root.appendChild( document.createElement(...) );
  • включением контента из основного документа: root.appendChild( document.querySelector('div#template').cloneNode(true) );
  • включением контента шаблона: root.appendChild( document.querySelector('template#template').content.cloneNode(true) );
  • комбинацией перечисленных способов.

Источники

CSS и Shadow DOM

Инкапсуляция стилей

Одной из ключевых особенностей Shadow DOM является инкапсуляция стилей. Каждое теневое дерево “окружено” невидимой границей Shadow Boundary (граница тени), которая отделяет его от основного дерева и других теневых деревьев. Это объясняет ограничения на доступ из основного дерева в теневое (и наоборот) при помощи обычных селекторов. Соответственно и стили, объявленные в Shadow DOM-дереве, инкапсулированы - т.е. действуют только внутри этого дерева.
Использование CSS внутри Shadow Boundary (для host-элементов, для distributed-элементов) имеет свои особенности. Рассмотрим основные.

Прозрачность Shadow Boundary

<button><span class="title">Simple Button</span></button>
<button class="bws">Button with shadow</button>
<template class="button">
    <style>.title { color:#f00; }</style>
    <div class="title"><content></content></div>
</template>
<script>
(function(){
    var host = document.querySelector('button.bws'),
        root = host.createShadowRoot(),
        template = document.querySelector('template.button');
    root.appendChild( template.content.cloneNode(true) );
})();
</script>

Как видно из примера, граница непрозрачна изнутри. Для обратного направления это неверно.
Проверим это.

<style>button { font-style: italic; }</style>
<button><span class="title">Simple Button</span></button>
<button class="bws">Button with shadow</button>
<template class="button">
    <style>.title { color:#f00; }</style>
    <div class="title"><content></content></div>
</template>
<script>
(function(){
    var host = document.querySelector('button.bws'),
        root = host.createShadowRoot(),
        template = document.querySelector('template.button');
    root.appendChild( template.content.cloneNode(true) );
})();
</script>

Таким образом, по-умолчанию, наследуемые стили влияют на содержимое ShadowRoot (Внимание! Только наследуемые стили).

Управление “прозрачностью” Shadow Boundary

Прозрачностью границы в направлении “снаружи внутрь” можно управлять при помощи свойств ShadowRoot-элемента:

  • root.resetStyleInheritance // (default:false)
    сбрасывает наследуемые стили в значения, определенные по-умолчанию для данного user-agent
  • root.applyAuthorStyles // (default:false) управляет применением внутри теневого дерева стилей, объявленных снаружи

Пробуем. Включим сброс наследуемых стилей.

<style>button { font-style: italic; }</style>
<button><span class="title">Simple Button</span></button>
<button class="bws">Button with shadow</button>
<template class="button">
    <style>.title { color:#f00; }</style>
    <div class="title"><content></content></div>
</template>
<script>
(function(){
    var host = document.querySelector('button.bws'),
        root = host.createShadowRoot(),
        template = document.querySelector('template.button');
    root.resetStyleInheritance = true;
    root.appendChild( template.content.cloneNode(true) );
})();
</script>

Действительно, в ShadowRoot элемента <button class="bws" /> стили были сброшены в дефолтные, применился только стиль <style>.title { color:#f00; }</style>, объявленный в шаблоне <template class="button" />.
Проверим управление применением внешних стилей внутри теневого дерева с помощью root.applyAuthorStyles.

<style>.blue { color: #00f; }</style>
<button><span class="blue">Simple Button</span></button>
<button class="bws">Button with shadow</button>
<template class="button">
    <span class="blue"><content></content></span>
</template>
<script>
(function(){
    var host = document.querySelector('button.bws'),
        root = host.createShadowRoot(),
        template = document.querySelector('template.button');
    root.applyAuthorStyles = true;
    root.appendChild( template.content.cloneNode(true) );
})();
</script>

Итак, root.applyAuthorStyles = true

Применим root.applyAuthorStyles = false

Управление стилями host-элемента. Селектор :host

Для задания стиля host-элемента из теневого дерева определен селектор :host.
Попробуем.

<button>Simple Button</button>
<button class="bws">Button with shadow</button>
<template class="button">
    <style>:host { background-color:#00f; }</style>
    <content></content>
</template>
<script>
(function(){
    var host = document.querySelector('button.bws'),
        root = host.createShadowRoot(),
        template = document.querySelector('template.button');
    root.appendChild( template.content.cloneNode(true) );
})();
</script>

Селектор :host можно применять в комбинации с селекторами классов, идентификаторов, тегов. В большинстве случаев это работает пока нестабильно.
Пробуем.

<button>Simple Button</button>
<button class="btnred">Red button</button>
<button class="btngreen">Green button</button>
<button class="btnblue">Blue button</button>
<template class="button">
    <style>
       :host { font-weight:bold!important; }
       :host(.btnred) { background-color:#f00!important; }
       :host(.btngreen) { background-color:#0f0!important; }
       :host(.btnblue) { background-color:#00f!important; }
    </style>
    <content></content>
</template>
<script>
(function(){
    var template = document.querySelector('template.button');
    document.querySelector('.btnred')
            .createShadowRoot()
            .appendChild( template.content.cloneNode(true) );
    document.querySelector('.btngreen')
            .createShadowRoot()
            .appendChild( template.content.cloneNode(true) );
    document.querySelector('.btnblue')
            .createShadowRoot()
            .appendChild( template.content.cloneNode(true) );
})();
</script>

Селектор :host можно применять в комбинации с селекторами псевдоклассов состояния :active, :hover.
Пробуем.

<button>Simple Button</button>
<button class="bws">Button with shadow</button>
<template class="button">
    <style>
       *:host { opacity:0.5!important; }
       *:host:hover { opacity:1!important; }
       *:host:active { background-color:#f00!important; }
    </style>
    <content></content>
</template>
<script>
(function(){
    var template = document.querySelector('template.button');
    document.querySelector('.bws')
            .createShadowRoot()
            .appendChild( template.content.cloneNode(true) );
})();
</script>

Управление стилями Distributed nodes. Селектор ::content

Обычные селекторы вида .title { color:#f00; }, объявленные внутри shadow boundary, не применяются к Distributed nodes.
Для явного указания в селекторе элемента что он применяется к Distributed nodes используют селектор ::content.

<button><span class="title">Simple Button</span></button>
<button class="button1"><span class="title">Button with template#1</span></button>
<button class="button2"><span class="title">Button with template#2</span></button>
<template class="template1">
    <style>.title { color:#f00; }</style>
    <content></content>
</template>
<template class="template2">
    <style>::content > .title { color:#f00; }</style>
    <content></content>
</template>
<script>
(function(){
    var template1 = document.querySelector('.template1'),
        template2 = document.querySelector('.template2'),
        template3 = document.querySelector('.template3');
    document.querySelector('.button1')
            .createShadowRoot()
            .appendChild( template1.content.cloneNode(true) );
    document.querySelector('.button2')
            .createShadowRoot()
            .appendChild( template2.content.cloneNode(true) );
})();
</script>

Источники

HTML Imports

Custom Elements

Теория

Применение

Текущий статус

Уже сейчас можно использовать возможности Web Components с помощью полифилов:

Существуют каталоги компонент и UI-элементов на базе X-Tag и Polymer:

Источники

Заключение

Источники


  1. В качестве “основных” направлений деятельности группы W3C WebApp в настоящее время выступают API Specifications, Web Components Specifications, Widget Specifications. 

  2. Одним из двух со-председателей группы является Charles McCathieNevile - сотрудник Yandex с 2012 года. 

comments powered by Disqus