Greetings, colleagues, and present to you the continuation of a series of articles on web components, the first part of which is available here
This article will discuss the specification of the shadow DOM (shadow DOM) version of 03/01/2018. The latest draft of the specification is dated 08/03/2018.
The API of the shadow DOM allows us to encapsulate the content of the page by placing the markup in a tree structure called the shadow tree, which, although it will be embedded in the DOM, will not be its full part in the context we are familiar with. ordinary descendants in the DOM. It is this API in the context of all API for creating web components that gives us the opportunity not only to hide the internal implementation of components, but also to encapsulate styles with minimal effort.
Shadow DOM is already used by browsers for the internal implementation of a number of elements. For example
<input type=”range”/>
when viewed in the console, it turns out to be not a single element, but the tree structure of ordinary HTML elements in the shadow DOM.The key concept in the shadow DOM concept is the shadow tree , the same “subtree” that is rendered into a document, but not in the DOM tree. The easiest thing for me was to view the shadow tree as something between a part of a document and a fragment (document fragment).
The root element of the shadow tree is the shadow root . This is the node on which the .attachShadow (obj) method was called, where obj is an object with settings that contains the mode property - access mode to the shadow DOM, which can be set to "open" (access to the shadow DOM is possible from the main document using the .shadowRoot property) or “closed” (access via .shadowRoot returns null), however, this article explains why it is meaningless to seriously rely on the closed mode . The draft specification of 03/02/2018 also provided for the setting by the delegatedFocus property, which sets whether focus from shadow host to shadow root will be delegated (true / false), but after 6 days this concept from the specification has been removed.
The .attachShadow () method attaches a shadow tree to the node and returns a ShadowRoot object.
The tree that owns the shadow root is called the light tree , and by the way, the light tree may well be another shadow tree.
Shadow tree can contain slots ( slot ) elements. This element is an analogue of documentFragment - when it is rendered in the DOM, the slot is replaced with its contents.
Special cases of the slot element operation differ depending on whether the value is specified in its name attribute.
When the name attribute of the slot element is assigned a value, the slot element when rendering the document will be replaced with those document elements (light tree) that have a slot attribute set to a value equal to the value of the name attribute of the slot element.
As an example, I took the code for the user element tab created when writing the previous article and made the following changes to it:
class TabNavigationItem extends HTMLElement { constructor() { super(); this._target = null; this.attachShadow({ mode: 'open' }); } //... }
In the constructor, a shadow DOM is created and attached. Now access to the shadow root will be possible through the call this.shadowRoot.
The next step, I made changes to the .render () method by changing the custom element markup:
render() { if(!this.ownerDocument.defaultView) return; this.shadowRoot.innerHTML = ` <a href="#${this._target}"><slot name="nav"></slot></a> `; }
It is important to note that the markup will now be passed to the value of the .innerHTML property of the this.shadowRoot object.
Now in the markup, when using a custom element, I can set the internal contents of the navigation element with the only condition, the nested elements must have the slot attribute equal to the value of the name attribute of the slot element in the shadow DOM (in our example, “nav”), otherwise they will not be displayed.
<tab-nav-item class="active" target="First tab"> <h1 slot="nav">First</h1> </tab-nav-item>
I will make similar changes to the TabContentItem class:
class TabContentItem extends HTMLElement { constructor() { super(); this._target = null; this.attachShadow({ mode: 'open' }); } render() { if(!this.ownerDocument.defaultView) return; this.shadowRoot.innerHTML = ` <div> <slot name="tab"></slot> </div> `; } }
In connection with the connection of the shadow DOM, the need to use the content attribute to display the contents of the tab has disappeared.
The use of such custom elements is as follows:
<tab-element> <nav> <tab-nav-item class="active" target="First tab"> <h1 slot="nav">First</h1></tab-nav-item> <tab-nav-item target="Second tab"><h2 slot="nav">Second</h2></tab-nav-item> <tab-nav-item target="Third tab"><h3 slot="nav">Third</h3></tab-nav-item> </nav> <tab-content-item class='active' target="First tab"> <div slot="tab"> <h3>Hi, I'm the first</h3> <h1>TAB</h1> </div> </tab-content-item> <tab-content-item target="Second tab"> <p slot="tab">second one!</p> </tab-content-item> <tab-content-item target="Third tab"> <style slot="tab"> .test { border: 1px solid yellow; padding: 20px } </style> <div slot="tab"> <div class="test">I'm the third</div> </div> </tab-content-item> </tab-element>
From this example, it can be seen that the component user is able to transfer any markup both inside the navigation element and inside the content element of the taba, but, I repeat, with the restriction in the form of the presence of the slot attribute on the markup elements (and in our case - with the same same value for each of the custom items). In my opinion, this is the case when the second variant of the behavior of the slot element is preferable, namely:
If the name on the slot element is not specified, then, by default, it is equal to an empty string and such a slot will be called the default slot . When rendering, it will be replaced with elements that do not have a slot attribute.
Therefore, I will remove the name attribute and its value from the markup specified in the render methods, and the slot attributes and their values ​​from the markup in the main document. Thus, all the markup between the two tags of the user element will be embedded in the default slot. Markup isolation code can be viewed here .
The styles specified inside the style tag of an element from the shadow DOM will be limited to it. CSS selectors from the external environment will not be applied to the contents of the shadow DOM, and its styles, accordingly, will not flow out, which allows us to use the simplest selectors, which, besides the convenience of writing, are also better in performance.
Custom web components can style themselves from the context of the shadow DOM using the selector : host . At the same time, the rules of such a selector can be rewritten by external styles of components, which will allow changing the styles when using components (adapt if necessary at the time of use). A : host (selector) allows the component to stylize the host when it matches the selector, which is used to visualize user actions and the state of the application. Also available is stylization by context : host-context (selector) which coincides with the component only when one of the ancestors of the component matches the selector (used for stylization due to the environment).
I decided to remove the styles responsible for defining the behavior of the tabs and default external styles and hide them in the shadow DOM, for which I inserted the style tags inside the markup of the TabNavigationItem class's .render () method:
render() { if (!this.ownerDocument.defaultView) return; this.shadowRoot.innerHTML = ` <style> :host{ padding: 10px; background-color: gray; border: 1px solid gray; } :host-context(.active) { background-color: #ccc; } a{ text-decoration: none; color: black; } </style> <a href="#${this._target}"><slot></slot></a> `; }
Similarly, with the TabrententItem class .render () method:
render() { if (!this.ownerDocument.defaultView) return; this.shadowRoot.innerHTML = ` <style> :host { display: none; padding: 20px; width: 100%; height: 50px; } :host-context(.active){ display: block; } </style> <div><slot></slot></div> `; }
The final tab code with the involved shadow DOM can be viewed here. Now tabs can take markup inside elements, and they themselves contain their own styles, but the .render () methods have become cumbersome. This I plan to fix next time, when considering the specification of templates.
The pseudo-element :: slotted (selector) is also available for use from the shadow DOM, which must select the elements of the nested upper element that matches the selector. Other CSS features in the shadow DOM can be found here .
My example with tabs is not the best way to demonstrate this, but the shadow DOM also has a behavior pattern for events. For example, here is a list of events that, according to the documentation, should always stop at the innermost shadow root: abort, error, select, change, load, reset, resize, scrol, selectstart. And click, dbclick and almost all other mouse events, wheel, blur, focus, focusin, focusout, keydown, keyup, all drag events and some others cross the border of the shadow DOM. I was not able to use it, but I think that this information may be useful.
Currently, support for the first version of the specification is implemented in Chrome , in Opera , partially in Safari and implemented in Firefox .
The first article: Web components. Part 1: Custom Elements
The third article: Web components. Part 3: html templates and imports
Thank you for your attention, please do not judge strictly. Regards Tania_N
Source: https://habr.com/ru/post/350872/
All Articles