Abstract: In-depth JS series 17.
- How JavaScript works: The inner structure of the Shadow DOM + How to write independent components!
- Author: Front-end xiaozhi
FundebugReproduced with authorization, copyright belongs to the original author.
This is the 17th article in a series devoted to exploring JavaScript and the components it builds.
If you missed the previous chapters, they can be found here:
- How JavaScript works: An overview of the engine, runtime, and call stack!
- How JavaScript works: Dive into the V8 Engine & 5 Tips for Writing Optimized Code!
- How JavaScript works: Memory Management + How to Handle 4 common Memory Leaks!
- How JavaScript works: Event loops and the rise of asynchronous programming + 5 ways to code better with async/await!
- How JavaScript works: Explore websocket and HTTP/2 with SSE + how to choose the right path!
- How JavaScript works: Compare to WebAssembly and its usage scenarios!
- How JavaScript works: Building blocks for Web Workers + 5 scenarios to use them!
- How JavaScript works: Service Worker lifecycle and Usage Scenarios!
- How JavaScript works: The Web push notification mechanism!
- How JavaScript works: Use MutationObserver to keep track of DOM changes!
- How JavaScript works: Render Engines and tips for optimizing their performance!
- How JavaScript works: Dive deep into the Web Layer + How to optimize Performance and security!
- How JavaScript works: CSS and JS animation fundamentals and how to optimize their performance!
- How JavaScript works: Parsing, Abstract Syntax Tree (AST) + 5 tips to speed up compilation!
- How JavaScript works: Delving into classes and inheriting internals + Converting between Babel and TypeScript!
- How JavaScript works: Storage Engine + How to choose the right storage API!
An overview of the
Web Components is a different set of technologies that allow you to create reusable custom elements whose functionality is encapsulated outside of your code that you can use in Web applications.
The Web component consists of four parts:
- Shadow DOM
- HTML Templates
- Custom Elements
- HTML Imports (HTML Imports)
This article mainly introduces Shadow DOM.
Shadow DOM is a tool designed to build component-based applications. Therefore, solutions to common problems in network development can be provided:
- Isolated DOM: The COMPONENT’s DOM is independent (for example, document.querySelector() does not return nodes in the component’s shadow DOM).
- Scoped CSS: Shadow DOM defines CSS within its scope. Style rules do not leak and page styles do not seep in.
- Composition: Design a declarative, tag-based API for components.
- Simplified CSS-scoped DOM means you can use simple CSS selectors with more generic ID/class names without worrying about naming conflicts.
Shadow DOM
This article assumes that you are already familiar with DOM and other Api concepts. If not, you can read a full article about it here — developer.mozilla.org… .
The Shadowed DOM is just a normal DOM, except for two differences:
-
How it is created/used
-
Behavior in relation to other parts of the page
Typically, you create DOM nodes and append them to other elements as children. With shadow DOM, you can create a scoped DOM tree that is attached to the element but separated from its own real children. This scoped subtree is called a shadow tree. The attached element is called the shadow host. Any items you add to the shadow will become local to the host element, including
Typically, you create DOM nodes and append them as child elements to another element. With shadow DOM, create a scoped DOM tree attached to the element, but separate from the actual child element. The subtrees of this scope are called shadow trees, and the attached elements are called shadow hosts. Anything added to the shadow tree becomes a local element of the host element, including
Create shadow DOM
The shadow root is a document fragment attached to a “host” element that obtains its shadow DOM by attaching the shadow root. To create a Shadowed DOM for an element, call element.attachShadow() :
var header = document.createElement('header');
var shadowRoot = header.attachShadow({mode: 'open'});
var paragraphElement = document.createElement('p');
paragraphElement.innerText = 'Shadow DOM';
shadowRoot.appendChild(paragraphElement);
Copy the code
The specification defines a list of elements that cannot host a shadow tree. The elements are selected for the following reasons:
- The browser has hosted its own internal Shadow DOM for this element (
<textarea>
,<input>
). - It makes no sense for an element to host shadow DOM (
<img>
).
For example, the following approach does not work:
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Copy the code
Light DOM
This is the tag written by the component user. This DOM is not inside the shadow DOM component; it is the actual child of the element. Suppose you’ve created a custom component called
that extends the native HTML button component, and you want to add an image and some text to it. The code is as follows:
<extended-button>
<! -- The image and span are extended -- Button's light DOM -->
<img src="boot.png" slot="image">
<span>Launch</span>
</extended-button>
Copy the code
The “extension-button” is a custom component defined with HTML called the Light DOM, which is added by the user.
The Shadow DOM here is the extension-button component you created. The Shadow DOM is a local component of a component that defines its internal structure, scoped CSS, and encapsulated implementation details.
Flat DOM tree
The browser distributes the user-created Light DOM to the Shadow DOM and renders the final product. Flat trees are the objects that will eventually be seen in DevTools and rendered on the page.
<extended-button>
#shadow-root
<style>...</style>
<slot name="image">
<img src="boot.png" slot="image">
</slot>
<span id="container">
<slot>
<span>Launch</span>
</slot>
</span>
</extended-button>
Copy the code
Template (Templates)
If you need to repeat the same tag structure on a Web page, it’s better to use some type of template than to repeat the same structure over and over again. This used to be possible, too, but the HTML
element (well supported in modern browsers) makes it much easier. This element and its contents are not rendered in the DOM, but you can reference it using JavaScript.
A simple example:
<template id="my-paragraph">
<p> Paragraph content. </p>
</template>
Copy the code
This does not appear on the page until javascript references it, and then appends it to the DOM as follows:
var template = document.getElementById('my-paragraph');
var templateContent = template.content;
document.body.appendChild(templateContent);
Copy the code
So far, there are other technologies that can implement similar behavior, but, as mentioned earlier, it’s nice to encapsulate it natively, and Templates has pretty good browser support:
Templates are useful on their own, but they work better with custom elements. The customElement Api can define a customElement and tell the HTML parser how to properly construct an element and perform the appropriate processing when the element’s attributes change.
Let’s define a Web component named < my-Paragraph > that uses the previous template as the content of its Shadow DOM:
customElements.define('my-paragraph'.class extends HTMLElement {
constructor() {
super(a);let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true)); }});Copy the code
The key point to note here is that we have added a clone of the template content to the shadow root, which is created using the Node.clonenode () method.
Because you append its contents to a Shadow DOM, you can include some style information in the template in the form of elements, and then encapsulate it in custom elements. If you just append it to the standard DOM, it won’t work.
For example, you can change the template to:
<template id="my-paragraph">
<style>
p {
color: white;
background-color: # 666;
padding: 5px;
}
</style>
<p>Paragraph content. </p>
</template>
Copy the code
Custom components can now be used like this:
<my-paragraph></my-paragraph>
Copy the code
The element
Templates have some disadvantages, mainly static content, which does not allow us to render variables/data, allowing us to write code in the same way that standard HTML templates are used. Slots are placeholders inside components that users can fill with their own tags. Let’s see how slot is used in the template above:
<template id="my-paragraph">
<p>
<slot name="my-text">Default text</slot>
</p>
</template>
Copy the code
If the contents of the slot are not defined when the element is included in the tag, or the browser does not support slots, < my-Paragraph > shows only the text “Default text”.
To define the contents of the slot, we should include an HTML structure in the < my-Paragraph > element where the value of the slot attribute defines the name of the slot for us:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
Copy the code
The element that can be inserted into a slot is called Slotable. When an element is inserted into a slot, it is called slotted.
Notice that in the above example, a element is inserted, which is a slotted element with an attribute slot that is equal to my-text and has the same value as the name attribute in the slot definition in the template.
After rendering in a browser, the code above builds the following flat DOM tree:
<my-paragraph>
#shadow-root
<p>
<slot name="my-text">
<span slot="my-text">Let's have some different text!</span>
</slot>
</p>
</my-paragraph>
Copy the code
Set the style
Components that use shadow DOM can be styled via the home page, define their own styles, or provide hooks (in the form of CSS custom properties) for the user to replace default values.
The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, I recommend a good BUG monitoring tool for youFundebug.
The style of the component definition
Scoped CSS is one of the biggest features of Shadow DOM:
- CSS selectors for external pages do not apply inside components
- Styles defined within components do not affect other elements of the page; they are scoped to host elements
The CSS selectors used inside the Shadow DOM are applied locally to the component. In effect, this means we can again use the public VID/class name without worrying about collisions elsewhere on the page. The best practice is to use simpler CSS selectors inside the Shadow DOM, which also perform well.
Take a look at some of the styles defined in #shadow-root:
#shadow-root
<style>
#container {
background: white;
}
#container-items {
display: inline-flex;
}
</style>
<div id="container"></div>
<div id="container-items"></div>
Copy the code
All styles in the above example are native to #shadow-root. Use the #shadow-root element to introduce stylesheets that are also local.
:host pseudo-class selector
Use the :host pseudo-class selector to select elements in the component host element (as opposed to elements inside the component template).
<style>
:host {
display: block; /* by default, custom elements are display: inline */
}
</style>
Copy the code
One thing to be careful of when it comes to the :host selector is that the rules in the parent page have a higher priority than the :host rules defined in the element, which allows the user to override top-level styles externally. Also: Host only works in the Shadow root directory, so you can’t use it outside the Shadow DOM.
If :host(
) matches the function form of
, you can specify the host. This is a good way for your component to encapsulate behavior based on how the host will react to user interaction or state, or to style internal nodes:
<style>
:host {
opacity: 0.4;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
Copy the code
:host-context()
:host-context(
) or any of its parents matches, which will match the component. For example, there might be a CSS class on an element of a document that represents a style theme, and we should base our decision on the style of the component. For example, many people do this by applying or subjecting classes:
<body class="lightheme">
<custom-container>
…
</custom-container>
</body>
Copy the code
In the following example, background-color styles are applied to all elements inside the component only if an ancestor element has the CSS class theme-light:
:host-context(.theme-light) h2 {
background-color: #eef;
}
Copy the code
/deep/
Component styles usually apply only to the HTML of the component itself. We can use the /deep/ selector to force a style to apply to the views of each level of the child component as well as to the content of the component.
In the following example, we target all elements, from the host element to the current element to all child elements in the DOM:
:host /deep/ h3 {
font-style: italic;
}
Copy the code
The /deep/ selector also has an alias >>> that can be used interchangeably.
/deep/ and >>> selectors can only be used in emulated ** mode. This is the default and most commonly used method.
Styling components externally
There are several ways to style components externally: the simplest way is to use the tag name as a selector, as follows
custom-container {
color: red;
}
Copy the code
External styles take higher priority than those defined in the Shadow DOM.
For example, if the user writes a selector:
custom-container {
width: 500px;
}
Copy the code
It will override the component’s style:
:host {
width: 300px;
}
Copy the code
Styling the component itself can only go so far. But what happens if one wants to style the interior of a component? To do this, we need CSS custom properties.
Create style hooks using CSS custom properties
If the component’s developer provides style hooks through CSS custom properties, the user can adjust the internal styles. The idea is similar to
, but applies to styles.
Consider the following example:
<! - the main page - >
<style>custom-container { margin-bottom: 60px; - custom - the container - bg: black; }</style>
<custom-container background>...</custom-container>
Copy the code
Inside its Shadow DOM:
:host([background]) {
background: var(- custom - the container - bg, # CECECE);border-radius: 10px;
padding: 10px;
}
Copy the code
In this case, the component will use black as the background value because the user specified it; otherwise, the background color will be the default #CECECE.
As a component author, it is your responsibility to let developers know what CSS custom properties they can use as part of the component’s public interface.
Use slot in JS
The Shadow DOM API provides utilities that use slot and distributed nodes, which will come in handy when writing custom elements.
Slotchange event
The SlotChange event is emitted when a slot’s distributed node changes. For example, if the user adds/removes child elements from the Light DOM.
var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange'.function(e) {
console.log('Light DOM change');
});
Copy the code
To monitor other types of changes to the Light DOM, you can use MutationObserver in the element constructor. The internal structure of MutationObserver and how to use it have been discussed previously.
AssignedNodes () method
Sometimes it is useful to know which elements are associated with slot. Call slot.assignedNodes() to see which elements are being rendered by slot. The {flatten: true} option returns the alternate contents of the slot (provided no nodes are distributed).
Let’s look at the following example:
<slot name="Slot1"><p>Default content</p></slot>
Copy the code
Suppose this is in a component named < my-Container >.
Take a look at the different uses of this component, and what happens when you call assignedNodes() :
In the first case, we will add our own content to the slot:
<my-container>
<span slot="slot1"> container text </span>
</my-container>
Copy the code
Calling assignedNodes() yields [ container text ]. Note that the result is an array of nodes.
In the second case, empty the content:
<my-container> </my-container>
Copy the code
Calling assignedNodes() returns an empty array [].
In the third case, we call slot.assignedNodes({flatten: true}) and get: [
default content
].
Additionally, to access elements in the slot, call assignedNodes() to see which component slot the element is assigned to.
The event model
It is worth noting what happens when events occur in the Shadow DOM bubble up.
When an event is fired from the Shadow DOM, its goal is adjusted to maintain the encapsulation provided by the Shadow DOM. That is, the target of events is redirected so that they appear to come from components rather than internal elements in the Shadow DOM.
Here is a list of events propagated from Shadow DOM (some are not):
- ** Focus events: blur, focus, focusIn, focusOut
- ** Mouse events: **click, dblclick, mousedown, mouseEnter, Mousemove, etc
- ** Wheel event: **wheel
- ** Input events: ** beforeInput, input
- ** Keyboard events: **keydown, keyup
- ** Combination events: ** comPOSItionStart, comPOSItionUpdate, comPOSItionEnd
- ** Drag events: **dragstart, drag, dragend, drop, and so on
Custom events
By default, custom events are not propagated outside the Shadow DOM. If you want to dispatch custom events and propagate them, you need to add the bubbles: True and Composed: True options.
Let’s see what an event like this looks like:
var container = this.shadowRoot.querySelector('#container');
container.dispatchEvent(new Event('containerchanged', {bubbles: true.composed: true}));
Copy the code
Browser support
If you want to obtain the shadow DOM detection function, please check whether attachShadow exists:
constsupportsShadowDOMV1 = !! HTMLElement.prototype.attachShadow;Copy the code
For the first time ever, we have API primitives that enforce proper CSS scoping, DOM scoping, and real combinations. Combined with other web component apis, such as custom elements, shadow DOM provides a way to write truly encapsulated components without much effort or use of archaic things like
About Fundebug
Fundebug focuses on real-time BUG monitoring for JavaScript, wechat applets, wechat games, Alipay applets, React Native, Node.js and Java online applications. Since its launch on November 11, 2016, Fundebug has handled more than 900 million error events, and paid customers include Google, 360, Kingsoft, Minming.com and many other brands. Welcome to try it for free!