# Shadow DOM, шаблоны и стили
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) является отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
*Shadow DOM* -- это внутренний DOM элемента, который существует отдельно от внешнего документа. В нём могут быть свои ID, свои стили и так далее. Причём снаружи его, без применения специальных техник, не видно, поэтому не возникает конфликтов.
[cut]
## Внутри браузера
Концепция Shadow DOM начала применяться довольно давно внутри самих браузеров. Когда браузер показывает сложные элементы управления, наподобие слайдера `` или календаря `` -- внутри себя он конструирует их из самых обычных стилизованных `
`, `` и так далее.
С первого взгляда они незаметны, но если в настройках Chrome Development Tools выбрать показ Shadow DOM, то их можно легко увидеть.
Например, вот такое содержимое будет у ``:
То, что находится под `#shadow-root` -- это и есть Shadow DOM.
**Получить элементы из Shadow DOM можно только при помощи специальных JavaScript-вызовов или селекторов. Это не обычные дети, а намного более мощное средство отделения содержимого.**
В Shadow DOM выше можно увидеть полезный атрибут `pseudo`. Он нестандартный, существует по историческим причинам. С его помощью можно стилизовать подэлементы через CSS, например, сделаем поле редактирования даты красным:
```html
```
Ещё раз заметим, что `pseudo` -- нестандартный атрибут. Если говорить хронологически, то сначала браузеры начали экспериментировать внутри себя с инкапсуляцией внутренних DOM-структур, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое разработчикам.
Далее мы рассмотрим работу с Shadow DOM из JavaScript, по стандарту [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/).
## Создание Shadow DOM
Shadow DOM можно создать внутри любого элемента вызовом `elem.createShadowRoot()`.
Например:
```html
Доброе утро, страна!
```
Если вы запустите этот пример, то увидите, что изначальное содержимое элемента куда-то исчезло и показывается только "Привет из подполья!". Это потому, что у элемента есть Shadow DOM.
**С момента создания Shadow DOM обычное содержимое (дети) элемента не отображается, а показывается только Shadow DOM.**
Внутрь этого Shadow DOM, при желании, можно поместить обычное содержимое. Для этого нужно указать, куда. В Shadow DOM это делается через "точку вставки" (insertion point). Она объявляется при помощи тега ``, например:
```html
Доброе утро, страна!
```
Теперь вы увидите две строчки: "Доброе утро, страна!" в заголовке, а затем "Привет из подполья".
Shadow DOM примера выше в инструментах разработки:
Важные детали:
Тег `` влияет только на отображение, он не перемещает узлы физически. Как видно из картинки выше, текстовый узел "Доброе утро, страна!" остался внутри `p#elem`. Его можно даже получить при помощи `elem.firstElementChild`.
Внутри `` показывается не элемент целиком `
`, а его содержимое, то есть в данном случае текст "Доброе утро, страна!".
**В `` атрибутом `select` можно указать конкретный селектор содержимого, которое нужно переносить. Например, `` перенесёт только заголовки.**
Внутри Shadow DOM можно использовать `` много раз с разными значениями `select`, указывая таким образом, где конкретно какие части исходного содержимого разместить. Но при этом дублирование узлов невозможно. Если узел показан в одном ``, то в следующем он будет пропущен.
Например, если сначала идёт ``, а затем ``, то в первом `` будут показаны заголовки `
` с классом `title`, а во втором -- все остальные, кроме уже показанных.
В примере выше тег `` внутри пуст. Если в нём указать содержимое, то оно будет показано только в том случае, если узлов для вставки нет. Например потому что ни один узел не подпал под указанный `select`, или все они уже отображены другими, более ранними ``.
Например:
```html
Новости
Жили-были старик со старухой, но недавно...
```
При запуске мы увидим, что:
Первый `` выведет заголовок.
Второй `` вывел бы автора, но так как такого элемента нет -- выводится содержимое самого ``, то есть "Без автора".
Третий `` выведет остальное содержимое исходного элемента -- уже без заголовка `
`, он выведен ранее!
Ещё раз обратим внимание, что `` физически не перемещает узлы по DOM. Он только показывает, где их отображать, а также, как мы увидим далее, влияет на применение стилей.
## Корень shadowRoot
После создания корень внутреннего DOM-дерева доступен как `elem.shadowRoot`.
Он представляет собой специальный объект, поддерживающий основные методы CSS-запросов и подробно описанный в стандарте как [ShadowRoot](http://w3c.github.io/webcomponents/spec/shadow/#shadowroot-object).
Если нужно работать с содержимым в Shadow DOM, то нужно перейти к нему через `elem.shadowRoot`. Можно и создать новое Shadow DOM-дерево из JavaScript, например:
```html
Доброе утро, страна!
```
[warn header="Внутрь встроенных элементов так \"залезть\" нельзя"]
На момент написания статьи `shadowRoot` можно получить только для Shadow DOM, созданного описанным выше способом, но не встроенного, как в элементах типа ``.
[/warn]
## Шаблоны
Элемент `` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность ``?
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только показывается, но и считается находящимся вообще "вне документа".
Однако, вместе с тем, оно всё же обрабатывается браузером (а значит должно быть корректным HTML) и записывается как `DocumentFragment` в свойство тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
```html
Доброе утро, страна!
Привет из подполья!
```
У нас получилось, что:
В элементе `#elem` содержатся данные в некоторой оговорённой разметке.
Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.
Это содержимое добавляется в Shadow DOM тега. Технически, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег ``.
Важные детали:
В отличие от вставки через `innerHTML` и от обычного `DocumentFragment`, скрипт внутри шаблона выполнится при вставке. Содержимое шаблона изначально "вне документа" и "оживает", когда оно попадает в него. Это относится ко всему -- картинки начинают загружаться, видео -- проигрываться и т.п.
Мы вставляем не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.
## Стили
Стилизация Shadow DOM покрывается более общей спецификацией ["CSS Scoping"](http://drafts.csswg.org/css-scoping/).
**По умолчанию стили внутри Shadow DOM относятся только к его содержимому.**
Например:
```html
Жили мы тихо-мирно, и тут...
Доброе утро, страна!
*!*
*/!*
Привет из подполья!
```
При запуске окрашенным в красный цвет окажется только `
` внутри Shadow DOM.
...Но при помощи специальных селекторов переходить через эту границу!
### Извне стиль для Shadow DOM
Если нужно со страницы стилизовать или выбрать элементы внутри Shadow DOM, то можно использовать селекторы:
**`::shadow` -- выбирает корень Shadow DOM.**
Например, `#elem::shadow div` найдёт внутри Shadow DOM `#elem` элементы `div`.
**`/deep/` -- особого вида CSS-селектор для всех элементов Shadow DOM, который полностью игнорирует границы между DOM'ами, включая вложенные подэлементы, у которых тоже может быть свой Shadow DOM.**
Например, `#elem /deep/ span` найдёт все `span` внутри Shadow DOM `#elem`, но кроме того, если в `#elem` есть подэлементы, у которых свой Shadow DOM, то оно продолжит поиск в них.
Вот пример, когда внутри одного Shadow DOM есть ``, у которого тоже есть Shadow DOM:
```html
```
Кроме того, на Shadow DOM действует CSS-наследование, если свойство поддерживает его по умолчанию.
В этом примере CSS-стили для `body` наследуются на внутренние элементы, включая Shadow DOM:
```html
```
Внутренний элемент станет красным курсивом.
[warn header="Нельзя получить содержимое встроенных элементов"]
Описанные CSS-селекторы можно использовать не только в CSS, но и в `querySelector`.
Исключением являются встроенные элементы типа ``, для которых CSS-селекторы работают, но получить их содержимое нельзя.
Например:
```html
```
[/warn]
### Стиль Shadow DOM в зависимости от хозяина
Следующие селекторы позволяют выбрать элемент-хозяин:
**`:host` выбирает элемент-хозяин**, в котором, живёт Shadow DOM.
**`:host(селектор хозяина)` выбирает элемент-хозяин, если он подходит под селектор.**
Например:
```css
:host(.important) {
/* сработает, если хозяин имеет класс important */
}
```
Этот селектор используется для темизации хозяина "изнутри", в зависимости от его классов и атрибутов.
**Хозяин :host выбирается в именно в контексте Shadow DOM.**
То есть, это доступ не к внешнему элементу, а, скорее, к корню текущего Shadow DOM.
После `:host(...)` мы можем указать селекторы и стили, которые нужно применить, если хозяин удовлетворяет тому или иному условию, например:
```html
```
Эти селекторы сработают для `
` внутри Shadow DOM, причём второй -- только если у хозяина стоит класс `important`.
**`:host-context(селектор хозяина)` выбирает элемент-хозяин, если какой-либо из его родителей удовлетворяет селектору.**
Например:
```css
:host-context(h1) p {
/* селектор сработает для p, если хозяин находится внутри h1 */
}
```
Это используется для расширенной темизации, теперь уже не только в зависимости от его атрибутов, но и от того, внутри каких элементов он находится.
Пример использования селектора `:host()` для темизации содержимого:
```html
*!*
Доброе утро, страна!
*/!*
*!*
Внимание-внимание! Говорит информбюро!
*/!*
```
### Стиль для содержимого
Тег `` не меняет DOM, а указывает, что где показывать. Поэтому если элемент изначально находится в элементе-хозяине -- внешний документ сохраняет к нему доступ.
К нему будут применены стили и сработают селекторы, всё как обычно.
Например, здесь применится стиль для ``:
```html
Доброе утро, страна!
Привет из подполья!
```
В примере выше заголовок "Доброе утро, страна!", который пришёл как `` из внешнего документа, будет подчёркнут,
...Но, поскольку эти узлы показываются внутри Shadow DOM, то ему тоже может понадобится к ним доступ.
**Для обращения к "содержимому" `` используется псевдоэлемент `::content`.**
Например, `content[select="h1"]::content span` найдёт элемент `` и *в его содержимом* отыщет ``.
Селектор `::content` подразумевает `*::content`, так что `::content span` стилизует все `` внутри всех ``.
Например:
```html
Доброе утро, страна!
Привет из подполья!
```
Если запустить пример выше, то текст внутри `
` станет зелёным и подчёркнутым одновременно.
Приоритет селекторов расчитывается по [обычным правилам специфичности](http://www.w3.org/TR/css3-selectors/#specificity), если же приоритеты стилей на странице и в Shadow DOM и на странице равны, то, как описано в секции [Cascading](http://dev.w3.org/csswg/css-scoping/#cascading), побеждает страница, а для `!important`-стиля побеждает Shadow DOM.
[summary]
Если обобщить -- инкапсуляция Shadow DOM имеет односторонний характер:
Изнутри Shadow DOM можно стилизовать только сам Shadow DOM и узлы, показываемые в ``.
Со страницы можно иметь доступ и стилизовать элементы, изначально находящиеся внутри хозяина -- напрямую, а узлы внутри Shadow DOM -- при помощи селекторов `::shadow` и `/deep/`.
[/summary]
## Итого
Shadow DOM -- это средство для создания отдельного DOM-дерева внутри элемента, которое не видно снаружи без применения специальных методов.
Ряд браузерных элементов со сложной структурой уже имеют Shadow DOM.
Можно создать Shadow DOM внутри любого элемента вызовом `elem.createShadowRoot()`. В дальнейшем его корень будет доступен как `elem.shadowRoot`.
Как только у элемента появляется Shadow DOM, его изначальное содержимое скрывается. Теперь показывается только Shadow DOM, который может указать, какое содержимое хозяина куда вставлять, при помощи элемента ``. Можно указать селектор `` и размещать разное содержимое в разных местах Shadow DOM.
Стили и `querySelector`, объявленные внутри Shadow DOM, по умолчанию относятся только к его содержимому, могут обращаться к содержимому ``, но не к основной странице.
Стили и `querySelector` с внешней страницы могут преодолевать границу между DOM при помощи селекторов `::shadow` и `/deep/`.
Спецификации, затрагивающие Shadow DOM:
[Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) -- самая полная спецификация по свойствам и методам Shadow DOM, деталям обработки событий.
[Introduction to Web Components](http://w3c.github.io/webcomponents/explainer/) -- обо всём понемногу.
[CSS Scoping](http://drafts.csswg.org/css-scoping/) -- спецификация по CSS-селекторам, в том числе Shadow DOM.