Components in the Browser: Shadow DOM

I like to describe Shadow DOM as kind of like an iframe, but with out the “frame”.

Up to now the only way to embed third party widgets in your page with true encapsulation is inside a very “expensive” iframe. What I mean by expensive, is that an iframe consumes the browser resources necessary to instantiate an entirely separate window and document. Even including an entirely empty <iframe> element in a page is orders of magnitude more resource intensive than any other HTML element. Examples of iframe widgets can be social networking buttons like a Google+ share, a Twitter tweet, a Facebook like, or a Disqus comments block. They can also include non-UI marketing and analytics related apps.

Often times an iframe makes sense from a security standpoint if the content is unknown or untrusted. In those situations, you want to be able to apply CSP (content security policy) restrictions to the embedded frame. But iframes are also the only choice if you need to encapsulate your widget’s look and feel from the styling in the parent page even if there are no trust issues. CSS bleed is the largest obstacle when it comes to developing portable UI components meant to propagate a particular set of branding styles. Even the HTML in such components can be inadvertently altered or removed by code in the containing page. Only JavaScript, which can be isolated in an anonymous self-executing function is relatively immune from outside forces.

http://www.w3.org/TR/shadow-dom/

http://w3c.github.io/webcomponents/spec/shadow

Shadow DOM is a W3C specification proposal that aims to fill the encapsulation gap in modern browsers. In fact, all major browsers already use it underneath some of the more complex elements like <select> or<input type="date">. These tags have their own child DOM trees of which they are rendered from. The only thing missing is an API giving the common web developer access to the same encapsulation tools.

LIKE AN EXTREMELY LIGHT-WEIGHT IFRAME

Shadow DOM provides encapsulation for the embedded HTML and CSS of a UI component. Logically the DOM tree in a Shadow DOM is seperate from the DOM tree of the page, so it is not possible to traverse into, select, and alter nodes from the parent DOM just like with the DOM of an embedded iframe. If there is an element with id=”comp_id” in the shadow DOM, you cannot use jQuery, $('#comp_id'), to get a reference. Likewise, CSS styles with that selector will not cross the shadow DOM boundary to match the element. The same holds true for certain events. In this regard, Shadow DOM is analogous to a really “light-weight” iframe.

That said, it is possible to traverse into the shadow DOM indirectly with a special reference to what is called theshadow root, and it is possible to select elements for CSS styling with a special pseudo class provided by the spec. But both of these tasks take conscious effort on the part of the page author, so inadvertent clobbering is highly unlikely.

JavaScript is the one-third of the holy browser trinity that behaves no different. Any <script> tags embedded in a shadow tree will execute in the same window context as the rest of the page. This is why shadow DOM would not be appropriate for untrusted content. However, given that JavaScript can already be encapsulated via function blocks in ES5 and via modules and classes in ES6, Shadow DOM provides the missing tools for creating high quality UI components for the browser environment.

Shadow DOM Concepts and Definitions

We have touched on the high-level Shadow DOM concepts above, but to really get our heads wrapped around the entire specification to the point where we can start experimenting, we need to introduce some more concepts and definitions. Each of these has a coresponding API hook that allows us to create and use shadow DOM to encapsulate our UI components. Please take a deep breath before continuing, and if you feel an anurysim coming on, then leave this for another day!

TREE OF TREES

The Document Object Model as we know it, is a tree of nodes (leaves). Adding shadow DOM means that we can now create a tree of trees. One tree’s nodes do not belong to another tree, and there is no limit to the depth. Trees can contain any number of trees. Where things get murky is when the browser renders this tree of trees. If you have been a good student of graph theory, then you’ll have a leg up groking these concepts.

Page Document Tree and Composed DOM

The final visual output to the user is that of a single tree which is called the composed DOM. However, what you see in the browser window will not seem to jibe with what is displayed in your developer tools, which is called the page document tree, or the tree of nodes above. This is because prior to rendering, the browser takes the chunk of shadow DOM and attaches it to a specified shadow root.

Shadow Root and Shadow Host

shadow root is a special node inside any element which “hosts” a shadow DOM. Such an element is referred to as a shadow host. The shadow root is the root node of the shadow DOM. Any content within a shadow hostelement that falls outside of the hosted shadow DOM will not be rendered directly as part of the composed DOM, but may be rendered if transposed to a content insertion point described as follows.

Content and Shadow Insertion Points

shadow host element may have regular DOM content, as well as, one or more shadow root(s). The shadow DOM that is attached to the shadow root may contain the special tags <content> and <shadow>. Any regular element content will be transposed into the <content> element body wherever it may be located in the shadow DOM. This is refered to as a content insertion point. This is quite analogous to using AngularJS ngTransclude except that rather than retaining the original $scope context, transposed shadow host content retains its original CSS styling. A shadow DOM is not limited to one content insertion point. A shadow DOM may have several <content> elements with a special select="css-selector" attribute that can match descendent nodes of the shadow host element, causing each node to be transposed to the corresponding content insertion point. Such nodes can be refered to as distributed nodes.

Not only may a shadow host element have multiple content insertion points, it may also have multiple shadow insertion points. Multiple shadow insertion points end up being ordered from oldest to youngest. The youngest one wins as the “official” shadow root of the shadow host element. The “older” shadow root(s) will only be included for rendering in the composed DOM if shadow insertion points are specifically declared with the <shadow> tag inside of the youngest or official shadow root.

Shadow DOM APIs

The following is current as of mid 2014. For the latest API information related to Shadow DOM please check the W3C link at the top of this section. The information below also assumes familiarity with the current DOM API and objects including: Document, DocumentFragment, Node, NodeList, DOMString, and Element.

From a practical standpoint we will start with the extensions to the current Element interface.

Element.createShadowRoot() – This method takes no parameters and returns a ShadowRoot object instance. This is the method you will likely use the most when working with ShadowDom given that there currently is no declarative way to instantiate this type.

Element.getDestinationInsertionPoints() – This method also takes no parameters and returns a staticNodeList. The NodeList consists of insertion points in the destination insertion points of the context object. Given that the list is static, I suspect this method would primarily be used for debugging and inspection purposes.

Element.shadowRoot – This attribute either referes to the youngest ShadowRoot object (read only) if the node happens to be a shadow host. If not, it is null.

ShadowRoot – This is the object type of any shadow root which is returned by `Element.createShadowRoot(). A ShadowRoot object inherits from DocumentFragment.

The ShadowRoot type has a list of methods and attributes, several of which are not new, and are the usual DOM traversal and selection methods (getElementsBy*). We will cover the new ones and any that are of particular value when working shadow dom.

ShadowRoot.host – This attribute is a reference to its shadow host node.

ShadowRoot.olderShadowRoot – This attribute is a reference to any previously defined ShadowRoot on the shadow host node or null if none exists.

HTMLContentElement <content> – This node type and tag inherits from HTMLElement and represents a content insertion point. If the tag doesn’t satisfy the conditions of a content insertion point it falls back to a HTMLUnknownElement for rendering purposes.

HTMLContentElement.getDistributedNodes() – This method takes no arguments and either returns a static node list including anything transposed from the shadow host element, or an empty node list. This would likely be used primarily for inspection and debugging.

HTMLShadowElement <shadow> – Is exactly the same interface as HTMLContentElement except that it describes shadow insertion point instead.

 

https://leanpub.com/web-component-development-with-angularjs/read

Leave a Reply