Доклад на 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 внутрь другого:
- для использования контента host-ноды в ее ShadowRoot служат content insertion points (точки вставки контента),
они задаются в разметке при помощи тега
<content />
и, соответственно, в Shadow DOM c помощью элементаHTMLContentElement
. - для использования контента предыдущего ShadowRoot служит shadow insertion point,
она задается в разметке при помощи тега
<shadow />
и, соответственно, в Shadow DOM c помощью элементаHTMLShadowElement
.
Важно, что точки вставки только указывают на место вставки контента другого дерева, а механизм распределения только отображает в них указанный ими контент не меняя ни одно из этих деревьев.
Ноды, распределенные в 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' ]; } ) );
};
Источники
- W3C Shadow DOM
- Intro to Shadow DOM
- HTML5 Rocks / Shadow DOM 101
- HTML5 Rocks / Shadow DOM 301
- http://robdodson.me/blog/2013/08/26/shadow-dom-introduction/
- http://robdodson.me/blog/2013/08/27/shadow-dom-the-basics/
- http://blog.teamtreehouse.com/working-with-shadow-dom
- http://code.tutsplus.com/tutorials/intro-to-shadow-dom--net-34966
- http://habrahabr.ru/post/180377/
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) )
; - комбинацией перечисленных способов.
Источники
- http://w3c.github.io/webcomponents/explainer/#template-section
- https://github.com/termi/CreativeWork/blob/WCE/RU_ru/Web%20Components%20Explained/Translation.md
- HTML5 Rocks / HTML’s New Template Tag
- http://blog.teamtreehouse.com/creating-reusable-markup-with-the-html-template-element
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-agentroot.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>
Источники
- W3C. Introduction to Web Components / 5.5 CSS and Shadow DOM
- http://robdodson.me/blog/2013/08/28/shadow-dom-styles/
- http://robdodson.me/blog/2013/08/29/shadow-dom-styles-cont-dot/
- HTML5 Rocks / Shadow DOM 201
HTML Imports
- http://www.html5rocks.com/en/tutorials/webcomponents/imports/
- http://robdodson.me/blog/2013/08/20/exploring-html-imports/
Custom Elements
Теория
Применение
Текущий статус
Уже сейчас можно использовать возможности Web Components с помощью полифилов:
- X-Tag - Web Components Custom Element Polylib (by Mozilla), GitHub
- Brick - UI Components for Modern Web Apps (by Mozilla), GitHub
- Polymer by Google
Существуют каталоги компонент и UI-элементов на базе X-Tag и Polymer:
Источники
- Custom Elements for Custom Applications – Web Components with Mozilla’s Brick and X-Tag
- http://www.html5rocks.com/en/tutorials/webcomponents/customelements/
- https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html
- http://coding.smashingmagazine.com/2014/03/04/introduction-to-custom-elements/
Заключение
Источники
- W3C. Introduction to Web Components
- http://www.evolutionoftheweb.com/?hl=ru
- http://www.chromestatus.com/
- http://habrahabr.ru/post/210058/
- Web Components Resources
- https://github.com/w3c/webcomponents
- html5-demos.appspot.com/webcomponents
- html5rocks
- A Guide to Web Components
- http://habrahabr.ru/post/152001/
- https://github.com/termi/CreativeWork/blob/WCE/RU_ru/Web%20Components%20Explained/Translation.md
- https://plus.google.com/103330502635338602217/posts
- http://updates.html5rocks.com/2013/03/What-s-the-CSS-scope-pseudo-class-for
- http://www.webcomponentsshift.com/
- http://jonrimmer.github.io/are-we-componentized-yet/
- http://markdalgleish.com/2013/11/web-components-why-youre-already-an-expert/
-
В качестве “основных” направлений деятельности группы W3C WebApp в настоящее время выступают API Specifications, Web Components Specifications, Widget Specifications. ↩
-
Одним из двух со-председателей группы является Charles McCathieNevile - сотрудник Yandex с 2012 года. ↩