📜 ⬆️ ⬇️

Let me introduce the Shadow DOM API based on slots.

I bring to your attention the translation of the article “Introducing Slot-Based Shadow DOM API” by Ryosuke Niwa, which he wrote on the WebKit blog last fall.

We are pleased to announce that the basic support for the new Shadow DOM API on the basis of slots , which we proposed in April (note of the translator: we are talking about April 2015) is already available in the WebKit nightly builds after r190680 . Shadow DOM is part of Web Components — a set of specifications originally proposed by Google to make it possible to create reusable widgets and components on the web. Shadow DOM, in particular, provides a lightweight encapsulation of the DOM tree, allowing you to create a parallel tree on an element, the so-called “shadow shadow tree”, which changes the drawing of the element without changing the DOM. Users of such a component cannot inadvertently change something in it, because its shadow tree is not a usual descendant of the host element. In addition, the effect of styles is also limited to the scope, which means that the CSS rules declared outside the shadow of the tree do not apply to elements inside such a tree, and the rules declared inside - to elements outside.

Isolation of styles

The first significant advantage of using shadow DOM is the isolation of styles. Imagine that we want to create our own progresbar. For example, as follows, we could use two nested divs to represent the progress bar itself and another div with text that shows the percent complete:
<style> .progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; } .progress > .bar { background: #9cf; height: 100%; } .progress > .label { position: absolute; top: 0; left: 0; width: 100%; text-align: center; font-size: 0.8rem; line-height: 1.1rem; } </style> <template id="progress-bar-template"> <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> <div class="bar"></div> <div class="label">0%</div> </div> </template> <script> function createProgressBar() { var fragment = document.getElementById('progress-bar-template').content.cloneNode(true); var progressBar = fragment.querySelector('div'); progressBar.updateProgress = function (newPercentage) { this.setAttribute('aria-valuenow', newPercentage); this.querySelector('.label').textContent = newPercentage + '%'; this.querySelector('.bar').style.width = newPercentage + '%'; } return progressBar; } </script> 
Pay attention to the template element , the use of which allows the author to include a snippet of HTML text to later be instantiated by creating a clone. This is the first feature of the “web component” that we implemented in WebKit; it was later included in the HTML5 specification . The template element in the document is allowed to appear anywhere (say, between table and tr ), and the content inside the template is inert and does not execute scripts and download images or any other resources. Thus, it will be enough for the user of this progress bar to instantiate and update it as shown below:
 var progressBar = createProgressBar(); container.appendChild(progressBar); ... progressBar.updateProgress(10); 

With such an implementation of the progress bar, there is one problem: both of his divs are available to anyone who wishes, and the styles are not limited to the framework of the element itself. For example, the progress bar styles defined for the CSS class progress will also be applied to the following HTML:
 <section class="project"> <p class="progress">Pending an approval</p> </section> 
And the styles of other elements will override the appearance of the progress bar:
 <style> .label { font-weight: bold; } </style> 
We could bypass these restrictions by giving the progression bar the name of the custom element, for example, custom-progressbar to limit the scope of styles, and then initialize all the other properties to all: initial , but there is a more elegant solution in the Shadow DOM world. The basic idea is to present the outer div as an extra layer of encapsulation so that users won't see what's going on inside (creating divs for the labels and the slider itself), the progress bar styles will not interfere with the rest of the page and vice versa. To do this, we need to first create a ShadowRoot by calling the attachShadow({mode: 'closed'}) method on the progress bar, and then insert the DOM nodes necessary for our implementation into it. Suppose we continue to use the div to set the host to this shadow root, then we can create a new div in the following way and attach shadow root:
 <template id="progress-bar-template"> <style> .progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; } .progress > .bar { background: #9cf; height: 100%; } .progress > .label { position: absolute; top: 0; left: 0; width: 100%; text-align: center; font-size: 0.8rem; line-height: 1.1rem; } </style> <div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> <div class="bar"></div> <div class="label">0%</div> </div> </template> <script> function createProgressBar() { var progressBar = document.createElement('div'); var shadowRoot = progressBar.attachShadow({mode: 'closed'}); shadowRoot.appendChild(document.getElementById('progress-bar-template').content.cloneNode(true)); progressBar.updateProgress = function (newPercentage) { shadowRoot.querySelector('.progress').setAttribute('aria-valuenow', newPercentage); shadowRoot.querySelector('.label').textContent = newPercentage + '%'; shadowRoot.querySelector('.bar').style.width = newPercentage + '%'; } return progressBar; } </script> 
Note that the style element is inside the template and will be shadowed into the shadow root along with the divs. This will limit the scope of the styles to this very shadow root. Similarly, the styles on the outside do not apply to the elements inside.
Tip : during debugging, it may be useful to have the mode open shadow DOM, in which the shadow root will be accessible through the property's shadowRoot property. For example, {mode: DEBUG ? 'open' : 'closed'} {mode: DEBUG ? 'open' : 'closed'}

Copies of slots

The attentive reader at this point probably wondered: why not do it with CSS and not get into the DOM? Styling is the concept of a representation, why do we add new elements to the DOM? In fact, the first public working draft of the CSS Scoping Module Level 1 defines the @scope rule for this very purpose. Why did we need another style isolation mechanism? A good reason for the implementation was that the elements implemented inside the component are hidden from external node traversal mechanisms, such as querySelectorAll and getElementsByTagName . Due to the fact that the default nodes inside shadow root are not detected by these APIs, component users may not think about the internal implementation of each component. Each component is represented as an opaque element, the implementation details of which are encapsulated inside its shadow DOM. Keep in mind that the shadow DOM in no way cares about cross-origin constraints as the iframe element does. If necessary, other scripts will be able to penetrate the shadow DOM. However, there is another reason why this mechanism appeared - composition. Suppose we have a list of contacts:
 <ul id="contacts"> <li> Commit Queue (<a href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br> One Infinite Loop, Cupertino, CA 95014 </li> <li> Niwa, Ryosuke (<a href="mailto:rniwa@webkit.org">rniwa@webkit.org</a>)<br> Two Infinite Loop, Cupertino, CA 95014 </li> </ul> 
and we want to add to each item of contact information from the list of beautiful things with the included scripts:

Instead of copying all this text into our own shadow DOM, we could use named slots to draw text in the code of our shadow DOM as follows without changing it:
 <template id="contact-template"> <style> :host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; } b { display: inline-block; width: 5rem; } </style> <b>Name</b>: <slot name="fullName"><slot name="firstName"></slot> <slot name="lastName"></slot></slot><br> <b>Email</b>: <slot name="email">Unknown</slot><br> <b>Address</b>: <slot name="address">Unknown</slot> </template> <script> window.addEventListener('DOMContentLoaded', function () { var contacts = document.getElementById('contacts').children; var template = document.getElementById('contact-template').content; for (var i = 0; i < contacts.length; i++) contacts[i].attachShadow({mode: 'closed'}).appendChild(template.cloneNode(true)); }); </script> 
Conceptually, slots are unfilled shadows in the shadow DOM that are filled by descendants of the host element. Each element is assigned to a slot with a name defined in the slot attribute:
 <ul id="contacts"> <li> <span slot="fullName">Commit Queue</span> (<a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br> <span slot="address">One Infinite Loop, Cupertino, CA 95014</span> </li> </ul> 
Thus, we attach our shadow root to li , and each span with a slot attribute is assigned to a slot with a corresponding name inside the shadow DOM. Take a closer look at the shadow DOM template:
 <b>Name</b>: <slot name="fullName"> <slot name="firstName"></slot> <slot name="lastName"></slot> </slot><br> <b>Email</b>: <slot name="email">Unknown</slot><br> <b>Address</b>: <slot name="address">Unknown</slot> 
This template has two slots with the names email and address , as well as a slot called fullName , which contains two other slots, firstName and lastName . The fullName slot uses the foldback technique when firstName and lastName displayed only if there are no nodes assigned to fullName . Although in this case exactly one node is assigned to each slot, we could assign multiple elements with the same slot attribute to the same slot, then they would be displayed in the same order in which they are located by the descendants of the host element. You can also use unnamed standard slots, those descendants of the host that do not have the slot attribute specified will fill them. When the browser renders this component, the content of li is replaced with the shadow DOM, and the slots inside it are replaced with the assigned nodes, as if the following DOM is actually displayed:
 <ul id="contacts"> <li> <!--shadow-root-start--> <b>Name</b>: <slot name="fullName"> <!--slot-content-start--> <span slot="fullName">Commit Queue</span> <!--slot-content-end--> </slot><br> <b>Email</b>: <slot name="email"> <!--slot-content-start--> <a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a> <!--slot-content-end--> </slot><br> <b>Address</b>: <slot name="address"> <!--slot-content-start--> <span slot="address">One Infinite Loop, Cupertino, CA 95014</span> <!--slot-content-end--> </slot> <!--shadow-root-end--> </li> </ul> 
As you can see, slot-based composition is a powerful tool that allows widgets to insert content into a page without cloning and modifying the DOM. With it, widgets can react to changes in their descendants without resorting to MutationObserver or any explicit notifications from scripts. In essence, composition turns DOM into a communication mechanism between components.
')

Host item styling

There is another point that should be noted in the previous example - a mystical pseudo-class :host :
 <template id="contact-template"> <style> :host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; } b { display: inline-block; width: 5rem; } </style> ... </template> 
This pseudo-class, as its name implies, applies to the shadow DOM host in which this rule is located. By default, author styles outside the shadow DOM have a higher priority than the styles inside the shadow DOM. This is done so that the “default styles” can be defined inside the component, and the component users can be allowed to override them when necessary. In addition, a component can define styles that are principally important for its display (such as, for example, width or display ) with the !important keyword. Any !important rules inside the shadow DOM are considered higher priority than those !important that are declared outside.

Further work

Much work is still ahead regarding the implementation of Web Components. We would like to allow the shadow DOM slots to be styled through the styles of the corresponding external nodes. There are also wishes to teach components to be embedded in the subject of the document, as well as to expose styled parts in the form of CSS pseudo-elements. In the long run, we would like to see an imperative DOM API for manipulating the purpose of slots, we have long suggested that we do it . We are also interested in adding shadow DOM with arbitrary (custom) elements . In short, custom elements API allows authors to associate JavaScript classes with a specific element name in HTML documents; A great way to ideomatically assign arbitrary behavior and shadow DOM. Unfortunately, at the moment there are several contradictory proposals about how and when to create arbitrary elements. To help steer the discussion in W3C, we plan to make a prototype in WebKit. To build packages and deliver Web Components, we are working on ES6 modules . Like Mozilla , we believe that the modules will radically change the attitude of the authors to the structuring of their pages. Ultimately, we would like to design an API for creating completely isolated web components with iframe-like security policies based on shadow DOM and arbitrary elements. In conclusion, I would like to note that we are really very proud of the fact that significant opportunities of Web Components appear in WebKit, we will continue to write about new opportunities that are emerging. If you have any questions, please contact me directly, @WebKit or John Davis .

Source: https://habr.com/ru/post/304112/


All Articles