Shadow DOM
Итак, что же такое shadow DOM:
Shadow DOM (или теневая модель документа) — часть документа, реализующая инкапсуляцию в DOM дереве. Она (теневая модель) является частью документа и встраивается непосредственно внутрь страницы.
Для упрощения отладки shadow DOM, в хроме можно включить отображение в веб-инспекторе (Settings — General — Show shadow DOM).
Надо заметить, что в стандарте реализуемая инкапсуляция называется функциональной, поскольку shadow DOM встраивается в документ и является одной из многих его частей, работающих «независимо» (более-менее независимо) друг от друга. Соответственно, при проектировании реализации, нужно было установить функциональные границы в дереве документа, чтобы как-то оперировать с множеством таких «независимых» фрагментов. Для решения проблемы инкапсуляции, и была введена новая абстракция — shadow DOM, позволяющая создавать несколько DOM деревьев в пределах одного родительского дерева и был разработан документ, описывающий ее.
Дочернее дерево размещается внутри некоторого элемента на странице. Функциональные границы между главным деревом документа и теневым называются shadow boundaries (теневые границы). Элемент, который размещает в себе теневое дерево, называется shadow host, а корень теневого дерево, соответственно, называется shadow root.
Во время рендеринга shadow tree занимает место содержимого shadow host (элемента).
Пример реализации в chromium:
Insertion points
Для композиции потомков shadow host и shadow tree используются insertion points. Insertion points определяют местонахождение потомков shadow host в shadow tree. При рендеринге shadow tree потомки проецируются в это место. Механизм, определяющий какие потомки shadow host будут спроецированы в insertion point называется distribution.
Псевдо-элемент ::distributed()
::distributed(selector) — функциональный псевдо-элемент принимающий относительный селектор в качестве аргумента. Он представляет отношение между insertion point в shadow tree и элементом, перенесенным в insertion point.
Реализация (chrome canary only):
Один shadow host может вмещать в себя несколько shadow tree — они будут отображены в порядке их добавления. Такой набор деревьев называется shadow stack. Более «старый» shadow tree так же можно переносить в другой shadow tree посредством shadow insertion point.
Reprojection (перепроецирование)
Перепроецирование это ситуация, при которой первое shadow tree уже имеет insertion point, а второй shadow tree имеет shadow insetion point, при этом контент, взятый из shadow host сначала проецируется в первом shadow tree, а затем во втором.
Псевдо-элементы (в контексте shadow DOM)
In certain situations, the author of a shadow tree may wish to designate one or more elements from that tree as a structural abstraction that provides additional information about the contents of the shadow tree.
В определенных ситуациях, автору shadow tree захочется назначить один или несколько элементов из shadow tree как стукртурную абстракцию, дающую дополнительную информацию о контенте shadow tree.
Что я понимаю как возможность использовать css селекторы вне shadow tree для доступа к элементам внутри него:
События
Некоторые события пропускаются через shadow boundary, некоторые нет. Исключение составляют mutation events — они вообще не должны возникать в shadow tree и, соответственно, переходить через shadow boundary. При прохождении события через shadow boundary у него меняется event.target для поддержания инкапсуляции.
Вот интересный пример:
События спроецированного элемента всплывают в shadow host, как-будто он все еще находится непосредственно внутри shadow host. События first-inner-element не всплывают в shadow host, в отличие от second-inner-element, который абсолютно спозиционирован и вынесен за пределы shadow host (при этом event.target сменился).
Стили
Есть два метода, позволяющие манипулировать стилями shadow tree:
shadowRoot.resetStyleInheritance (false by default)
Сбрасывает наследование стилей для shadow tree (стили снаружи не применяются на shadow tree).
shadowRoot.applyAuthorStyles (false by default)
Применяет стили авторского (главного) документа.
Можно сказать, что некоторой «инкапсуляции» для html не хватало. Это открывает большие возможности по созданию и шаблонизации различных, заранее подготовленных, виджетов на странице. Удивляет только отсутствие инкапсуляции JavaScript кода внутри виджетов, хотя мне казалось бы это довольно логичным.
Веб-компоненты. Часть 2: теневой DOM
Вступление
Приветствую, коллеги, и представляю вашему вниманию продолжение серии статей о веб-компонентах, первая часть которой доступна вот тут
В этой статье речь пойдет о спецификации теневого DOM (shadow DOM) версии от 01.03.2018 г.. Последний черновик спецификации датирован 08.03.2018г.
АПИ теневого DOM позволяет нам инкапсулировать содержимое страницы, посредством помещения разметки в древовидную структуру, называемую shadow tree, которая, хотя и будет внедрена в DOM, не будет ее полноправной частью в привычном нам контексте: ее нельзя получить для взаимодействия стандартными методами js для работы с обычными потомками в DOM. Именно это АПИ в разрезе всех АПИ для создания веб-компонентов, дает нам возможность не только скрывать внутреннюю реализацию компонентов, но и инкапсулировать стили с минимальными усилиями.
Теневой DOM уже используется браузерами для внутренней реализации работы ряда элементов. Так например
при рассмотрении в консоли оказывается не единичным элементом, а именно древовидной структурой обычных HTML элементов в теневом DOM.
Основные понятия
Ключевым понятием в концепции теневого DOM является shadow tree, то самое «поддерево», которое рендерится в документ, но не находится в DOM дереве. Мне проще всего было рассматривать shadow tree как нечто среднее между частью документа и фрагметом (document fragment).
Дерево, которому принадлежит shadow root называют light tree, и, кстати light tree вполне может быть другим shadow tree.
Shadow tree может содержать элементы слоты (slot).Этот элемент — аналог documentFragment — при его рендеринге в DOM, слот заменяется на его содержимое.
Частные случаи работы элемента slot различаются в зависимости от того, задано ли значение в его атрибут name.
Если значение атрибута name задано
Когда атрибутe name элемента slot присвоено значение, то элемент slot при рендеринге документа будет заменен на те элементы документа (light tree), у которых есть атрибут slot, установленный в значение равное значению атрибута name у элемента slot.
В качестве примера я взяла код пользовательского элемента таб, составленный при написании предыдущей статьи и внесла в него следующие изменения:
В конструкторе создан и прикреплен теневой DOM. Теперь доступ к shadow root будет возможен через обращение this.shadowRoot.
Теперь в разметке при использовании пользовательского элемента я смогу задавать внутреннее содержимое элемента навигации с единственным условием, вложенные элементы должны иметь атрибут slot равный значению атрибута name у элемента slot в теневом DOM (в нашем примере — «nav»), иначе они не будут отображаться.
Аналогичные изменения я внесу в класс TabContentItem:
В связи с подключением теневого DOM необходимость в использовании атрибута content для отображения содержимого таб отпала.
Использование таких пользовательских элементов выглядит так:
Из этого примера видно, что пользователь компонента способен передать любую разметку как внутрь элемента навигации, так и внутрь элемента содержимого табы, но, повторюсь, с ограничением в виде обязательного наличия у элементов такой разметки атрибута slot (и в нашем случае — с одним и тем же значением для каждого из пользовательских элементов). На мой взгляд, это тот случай, когда второй вариант поведения элемента slot предпочительнее, а именно:
Если значение атрибута name не задано
Если имя на элементе slot не задано, то, по умолчанию, оно равно пустой строке и такой слот будут называть default slot. При рендеринге, он будет заменен на те элементы, которые не имеют атрибута slot.
Потому я уберу из разметки, задаваемой в методах render, атрибут name и его значение, а из разметки в основном документе атрибуты slot и их значения. Таким образом, вся разметка, указываемая между двумя тегами пользовательского элемента будет внедрена в дефолтный слот. Код изоляции разметки можно посмотреть вот тут.
Стили
Стили, указанные внутри тега style элемента из теневого DOM будут им ограничены. CSS селекторы из внешнего окружения не будут применяться к содержимому теневого DOM, а его стили, соответственно, не вытекут наружу, что позволяет нам использовать простейшие селекторы, которые, кроме удобства написания также лучше по производительности.
Пользовательские веб-компоненты могут стилизовать сами себя из контекста теневого DOM, используя селектор :host. При этом, правила такого селектора могут быть переписаны внешними стилями компоненты, что позволит менять стили при использовании компоненты (адаптировать при необходимости в момент использования). А :host(selector) позволяет компоненте стилизовать хост при его совпадении с селектором, что как раз используется для визуализации действий пользователя и состояния приложения. Также доступна стилизация по контексту :host-context(selector) который совпадает с компонентой только когда какой-то из предков компоненты совпадает с селектором (используется для стилизации обусловленной окружением).
Для использования из теневого DOM также доступен псевдоэлемент ::slotted(selector), который должен выбирать элементы вложенного верхнего элемента, совпадающего с селектором. О других возможностях CSS в теневом DOM можно почитать тут.
О событиях
Мой пример с табами не лучшим образом подходит для демонстрации этого, но теневой DOM имеет также особенность поведения событий. Так, например, вот список событий, которые, согласно документации, должны будут всегда останавливаться на самом внутреннем shadow root: abort, error, select, change, load, reset, resize, scrol, selectstart. А click, dbclick и почти все остальные мышиные события, wheel, blur, focus, focusin, focusout, keydown, keyup, все события перетаскивания (drag events) и некоторые другие свободно пересекают границу shadow DOM. Мне не доводилось это использовать, но я считаю что эта информация может пригодится.
О поддержке
В настоящее время поддержка первой версии спецификации реализована в Chrome, в Opera, частично в Safari и имплементируется в Firefox.
Спасибо за внимание, прошу не судить строго. С уважением Tania_N
Теневой DOM и события
Смысл создания теневого DOM-дерева – это инкапсуляция внутренних деталей компонента.
Поэтому, чтобы не нарушать инкапсуляцию, браузер меняет у этого события целевой элемент.
Рассмотрим простой пример:
Если нажать на кнопку, то выведется следующее:
Подмена целевого элемента не происходит, если событие берёт начало на элементе из слота, который фактически находится в обычном, светлом DOM.
Например, если пользователь кликнет на в примере ниже – целевой элемент события будет именно этот span для обоих обработчиков – теневого и обычного (светлого):
Всплытие и метод event.composedPath()
Для обеспечения всплытия событий используется развёрнутый DOM.
Таким образом, если у нас есть элемент в слоте, и событие происходит где-то внутри него, то оно всплывает до и выше.
В примере выше развёрнутое DOM-дерево будет таким:
Если теневое DOM-дерево было создано с
Этот метод следует тем же принципам, что и остальные. Внутреннее устройство закрытых DOM-деревьев совершенно скрыто.
Свойство: event.composed
Большинство событий успешно всплывают сквозь границу теневого DOM. Но не все.
Если посмотреть в спецификацию UI Events, то большинство событий имеют composed: true :
Хотя есть и события, имеющие composed: false :
Эти события могут быть пойманы только на элементах того же DOM, в котором находится целевой элемент события.
Генерация событий
Например, здесь мы создаём элемент div#inner в теневом DOM-дереве элемента div#outer и генерируем на нём два события. Только одно с флагом composed: true выйдет наружу, в документ:
Итого
У некоторых встроенных событий всё же стоит composed: false :
Эти события могут быть пойманы только на элементах, принадлежащих тому же DOM-дереву.
Введение в Shadow DOM
Russian (Pусский) translation by AlexBioJS (you can also view the original English article)
Инкапсуляция – один из китов, на которые опирается парадигма объектно-ориентированного программирования, и обычно используется для ограничения внутреннего представления объекта от внешнего мира.
Возвращаясь к нашей проблеме, скажу, что мы безусловно можем инкапсулировать код JavaScript при помощи замыканий или паттерна модуль, однако можем ли мы применить этот же подход и к разметке HTML? Представьте, что нам необходимо создать виджет UI (* пользовательский интерфейс); можем ли мы скрыть детали реализации нашего виджета от кода JavaScript и CSS, подключенного на странице, который пользуется нашим виджетом? Или наоборот, можем ли мы предотвратить нарушение работы нашего виджета или нарушения его внешнего вида из-за влияния кода, который пользуется нашим виджетом.
Решение проблемы при помощи Shadow DOM
Единственное решение, при котором создается граница между написанным вами кодом и кодом, который его использует, довольно безобразное и заключается в использовании громоздкого элемента iFrame, многие атрибуты которого считаются устаревшими (* является дочерним контекстом для браузера, за счет которого в текущую страницу встраивается другая страница HTML). Так должны ли мы всегда приспосабливаться к этому подходу?
Уже нет! Shadow DOM предоставляет нам элегантный способ перекрытия обычного поддерева DOM специальным фрагментом документа (* интерфейс DocumentFragment представляет собой минимальный объект документа, у которого нет родителя. Используется для хранения сегмента структуры документа, состоящей из узлов. Поскольку он не является частью древовидной структуры активного документа, то изменения, внесенные во фрагмент, не влияют на документ), который содержит другое дерево узлов, недоступных для скриптов и стилей. Интересно то, что это вовсе не новинка! Различные браузера уже использовали эту методологию для реализации нативных виджетов вроде date (* виджет для выбора времени), sliders (* слайдер), audio (* аудиоплеер), video player (* видеоплеер) и т.д.
Активируем показ Shadow DOM
На момент написания этого руководства текущая версия Chrome (v29) поддерживает инспектирование Shadow DOM при помощи Chrome DevTools. Откройте Devtools и нажмите кнопку с иконкой зубчиков шестеренки справа внизу экрана для открытия панели Settings, прокрутите бегунок немного вниз и увидите флажок для активации показа Shadow DOM.

Теперь, когда мы активировали его в нашем браузере, давайте ознакомимся с внутренним устройством аудиоплеера по умолчанию. Просто введите:
в вашу HTML-разметку. Благодаря нему показывается следующий нативный аудиоплеер в браузерах, что его поддерживают:

Теперь давайте, проинспектируйте этот виджет аудиоплеера, который только что создали. Вау!

В инспекторе показывается внутренняя организация аудиоплеера, которую без активации показа Shadow DOM было скрыто. Как мы видим в элементе audio используется фрагмент документа для размещения внутреннего контента виджета и его добавления в элемент-контейнер (известный как Shadow Host).
Shadow Host и Shadow Root

После выполнения кода для добавления Shadow DOM дерево Shadow заменяет контент Shadow Host.

Этот процесс перекрытия узлов часто называют композицией (* в ООП – метод создания нового объекта путём объединения старых и новых частей, в противоположность наследованию).
Hello Shadow DOM World (* первая простейшая программа, которую создают новички в области компьютерных наук)
Хватит болтовни, давайте приступим к написанию некоторого кода. Предположим, что у нас имеется следующая разметка, при помощи которой показывается простое сообщение с приветствием.
Добавьте следующий код JavaScript или воспользуйтесь Fiddle (* фрагмент кода HTML, CSS или JavaScript, размещаемый в онлайн сообществе JSFiddle):
Обратите внимание на префикс, специфичный для определенного производителя браузеров, – webkit перед именем функции. Он указывает на то, что эта функциональная возможность (* Shadow DOM) сейчас поддерживается только в некоторых браузерах, работающих на основе webkit (* движок браузера).
Если бы вы выполнили код этого примера в браузере с поддержкой Shadow DOM, то увидели бы «Hello Shadow DOM World» вместо «Welcome to My World», поскольку узлы Shadow DOM перекрыли бы обычные.
Оговорка: Как некоторые из вас могли заметить, мы смешиваем разметку с кодом скрипта, что обычно не рекомендуется, и Shadow DOM не является исключением. Мы преднамеренно не использовали шаблоны сейчас (* позволяет вам объявлять фрагменты DOM, которые подвергаются парсингу, неактивны при загрузке страницы и могут быть активированы позже при выполнении кода), чтобы избежать путаницы. Shadow DOM также предоставляет нам решение этой проблемы, и мы рассмотрим его очень скоро.
Некоторые сведения относительно Shadow Boundary
Если вы попробуете обратиться к контенту отображенного дерева при помощи JavaScript следующим образом:
то получите изначальный контент «Welcome to My World», а не контент, который собственно отображается на странице, поскольку дерево Shadow DOM инкапсулировано от любых скриптов. Это также означает, что виджет, который вы создаете при помощи Shadow DOM, защищен от влияния любых нежелательных/несовместимых скриптов, уже присутствующих на странице.
Инкапсуляция стилей
Подобным образом, пересечение Shadow Boundary любым селектором CSS запрещено. Ознакомьтесь со следующим кодом, в котором мы применили к элементам списка красный цвет, однако этот стиль применяется только к узлам, которые являются частью родительской страницы, и на элементы списка, которые являются частью Shadow Root, он не влияет.
Вы можете увидеть код в действии на Fiddle. Подобная инкапсуляция срабатывает даже если мы меняем направление обхода дерева (* при применении стилей). Любые стилевые правила, определенные внутри Shadow DOM, не влияют на родительский документ и остаются только в области видимости Shadow Root. Ознакомьтесь с этим Fiddle в качестве примера, где мы применяем синий цвет к элементам списка в Shadow DOM, однако на элементы списка родительского документа этот стиль не влияет.
Но при этом имеется одно примечательное исключение; Shadow DOM предоставляет нам возможность задать стилевое оформление для Shadow Host, узла DOM, в котором располагается Shadow DOM. В идеале этот узел располагается за пределами Shadow Boundary и не является частью Shadow Root, однако за счет использования правила @host мы можем добавить стилевое оформление к Shadow Host так, как мы сделали для сообщения с приветствием в примере ниже.
Ознакомьтесь с этим Fiddle, где мы добавили стилевое оформление для сообщения с приветствием в Shadow Host при помощи стилевых правил, заданных в Shadow DOM.
Создание зацепок (* специальная точка входа; место в программе, куда можно подсоединить дополнительный код (обычно для расширения ее функциональных возможностей)) для добавления стилевого оформления
Как разработчик виджета я мог бы захотеть, чтобы у пользователя моего виджета была возможность добавить собственное стилевое оформление для определенных элементов. Это реализуемо за счет добавления дырки в Shadow Boundary при помощи пользовательских псевдо-элементов (* элемент DOM внутри Shadow Root пользовательского элемента, для которого автор явно добавил возможность задания правил стилевого оформления из-за пределов Shadow Root при помощи псевдо-селекторов). Это подобно тому, как некоторые браузера предоставляют разработчикам зацепки для добавления стилевого оформления к некоторым внутренним элементам нативного виджета. Например для того чтобы добавить стилевое оформление для ползунка и линейки нативного слайдера, вы можете использовать ::-webkit-slider-thumb и ::webkit-slider-runnable-track следующим образом:
Форкните (* создайте собственную копию) этого Fiddle и добавьте собственное стилевое оформление к нему.
Переориентация события
Если событие, которое возникает на одном из узлов Shadow DOM, пересекает Shadow Boundary, то оно переориентируется таким образом, чтобы относиться к Shadow Host для сохранения инкапсуляции. Рассмотрим следующий код:
Разделяем то, о чем нужно будет позаботиться
Мы уже знаем, что всегда следует разделять собственно контент и его представления; в Shadow DOM не должен содержаться никакой контент, который необходимо в итоге показать пользователю. Вместо этого контент должен всегда располагаться на исходной странице, а не скрываться в шаблоне Shadow DOM. При выполнении композиции этот контент затем должен быть спроецирован в соответствующую точку вставки (* место в документе, где будут выполняться некоторые действия), указанную в шаблоне Shadow DOM. Давайте перепишем наш пример «Hello World», помня о вышеупомянутом разделении; с рабочим примером можете ознакомиться на Fiddle.
Ознакомьтесь с рабочим примером и поэкспериментируйте с ним, чтобы лучше понять концепцию точек вставки и проецирования.
Web Components
Как вы уже, скорее всего, знаете, Shadow DOM – часть спецификации Web Components, которая предлагает и другие изящные инструменты вроде:
Я не буду долго болтать о других аспектах спецификации Web Components в этом руководстве, однако хорошо, если мы будем помнить, что вместе они позволяют нам создавать виджеты UI, которые можно повторно использовать и которые выглядят одинаково в различных браузерах, а также полностью инкапсулированы от всех скриптов и стилей страницы, которая использует их.
Заключение
В настоящий момент только Chrome и Opera поддерживают эту технологию, так что я был бы настороже при добавлении какого-либо Shadow DOM в мои конечные работы, однако учитывая то, что Google предоставляет Polymer, работающий на основе Web Components, и Polyfills (* polyfill – тип прокладки, которая добавляет в старые браузеры поддержку возможностей, которые в современных браузерах являются встроенными), предназначенный для нативной поддержки Shadow DOM браузерами, эту технологию должен осваивать каждый веб-разработчик.
Вы также можете следить за событиями, происходящими в мире Shadow DOM, подписавшись на канал Google+ Channel. Также ознакомьтесь с инструментом Shadow DOM Visualizer (* визуализатор работы Shadow DOM), который помогает вам визуализировать то, как Shadow DOM отображается в браузере.
ShadowRoot
The ShadowRoot interface of the Shadow DOM API is the root node of a DOM subtree that is rendered separately from a document’s main DOM tree.
Properties
Returns the Element within the shadow tree that has focus.
Returns a boolean that indicates whether delegatesFocus was set when the shadow was attached (see Element.attachShadow() ).
The element that’s currently in full screen mode for this shadow tree.
Returns a reference to the DOM element the ShadowRoot is attached to.
Returns the Element within the shadow tree that is currently being presented in picture-in-picture mode.
Returns the Element set as the target for mouse events while the pointer is locked. null if lock is pending, pointer is unlocked, or if the target is in another tree.
Returns a StyleSheetList of CSSStyleSheet objects for stylesheets explicitly linked into, or embedded in a shadow tree.
Event handlers
An event handler representing the code to be called when the slotchange event is raised.
Methods
Returns an array of all Animation objects currently in effect, whose target elements are descendants of the shadow tree.
Returns a Selection object representing the range of text selected by the user, or the current position of the caret.
Returns the topmost element at the specified coordinates.
Returns an array of all elements at the specified coordinates.
Examples
The following snippets are taken from our life-cycle-callbacks example (see it live also), which creates an element that displays a square of a size and color specified in the element’s attributes.










