preface

Shadow DOM is one of the main technical Components of Web Components. You can attach a hidden, independent DOM to an element [1]. You can create a Web component (hereinafter referred to as a Shadow DOM component) that is isolated from the rest of the page and reusable without worrying about collisions. Its basic concept and basic usage can be obtained from the MDN document, and will not be described here.

This paper mainly discusses the pain points in the process of developing a complex Shadow DOM component, and provides an idea to reduce the complexity of writing Shadow DOM components.

The development of pain points

Using popup-info-box🌰 on MDN, you can see that the complexity of Shadow DOM is mainly reflected in the following aspects:

1. Create a Shadow DOM structure

The Shadow DOM is created by mounting a Shadow DOM via element.attachShadow () and returning a reference to ShadowRoot. Then add children to the shadow root node, set properties, and so on, just as you would with a normal DOM. Create the Shadow DOM structure by performing the appendChild operation on the ShadowRoot object:

// Create shadow root
var shadow = this.attachShadow({mode: 'open'});
/ / create a span
var wrapper = document.createElement('span');
wrapper.setAttribute('class'.'wrapper');
var icon = document.createElement('span');
icon.setAttribute('class'.'icon');
icon.setAttribute('tabindex'.0);
var info = document.createElement('span');
info.setAttribute('class'.'info');

// Get the content of the attribute and add the content to the info element
var text = this.getAttribute('text');
info.textContent = text;

/ / insert icon
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

// Add the created element to the Shadow DOM
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
Copy the code

A lot of code is required to batch appendChild like this. If it is a static HTML structure, it is better to create a root node and execute the appendChild operation once:

const template = document.createElement('template');
template.innerHTML = `    `;

shadow.appendChild(template.content.cloneNode(true));
Copy the code

With innerHTML and template strings, you can describe the structure of the Shadow DOM declaratively, making it much more readable. But because it’s just a string, the editor doesn’t highlight its syntax, and as the DOM structure gets more complex, maintainability and readability get worse.

2. Add styles for Shadow DOM

To style the Shadow DOM, you can create a

// Add some CSS styles for shadow DOM
var style = document.createElement('style');

style.textContent = ` .wrapper { position: relative; }.info {font-size: 0.8rem; width: 200px; display: inline-block; border: 1px solid black; padding: 10px; background: white; border-radius: 10px; opacity: 0; The transition: 0.6 s all; position: absolute; bottom: 20px; left: 10px; z-index: 3; } img {width: 1.2rem; } .icon:hover + .info, .icon:focus + .info { opacity: 1; } `;
Copy the code

Then, it is added to Shadow root again via the appendChild operation:

shadow.appendChild(style);
Copy the code

This is almost exactly the same as the previous DOM structure, with no guarantee of maintainability and readability, and no CSS preprocessor support, which greatly reduces development efficiency.

In addition to styling Shadow DOM inline

// Add externally referenced styles to Shadow DOM
const linkElem = document.createElement('link');
linkElem.setAttribute('rel'.'stylesheet');
linkElem.setAttribute('href'.'style.css');

// Add the created element to the Shadow DOM

shadow.appendChild(linkElem);
Copy the code

But because the element does not interrupt shadow root’s drawing, unstyled content (FOUC) may appear when the stylesheet is loaded, causing flickering [1]. Referring to external styles will also cause the presentation of Shadow DOM to depend on external to some extent, thus losing the meaning of encapsulation and its independence. Therefore, the inline

The solution

In order to solve the above pain points, the author decided to use Webpack to help build Shadow DOM components. The general idea is to take traditional Web development thinking, separate structure (HTML) and presentation (CSS), and then assemble them into the Shadow DOM components we need. Before the formal transformation, create a Webpack project:

npm init
npm install --save-dev webpack
npm install --save-dev webpack-cli
Copy the code

1. Use htML-loader to separate Shadow DOM structures

Html-loader is a Webpack loader that exports HTML as a string, imports it in an entry file, and assigns values to innerHTML to form a Shadow DOM structure. First, install dependencies:

npm install --save-dev html-loader
Copy the code

Then configure the plug-in in the Webpack configuration file. Because innerHTML sets the descendant of the element represented by HTML syntax, we don’t need to create a complete HTML structure for this, or even have a root. The Webpack configuration is as follows:

// wepback.config.js
module.exports = {
  mode: 'production'.entry: './src/index.js'.output: {
    filename: 'popup-info.js'
  },
  module: {
    rules: [{test: /\.html$/i,
        loader: 'html-loader'},],}};Copy the code

If mode is set to production, html-loader compresses HTML in production mode. Create index.js and template. HTML files under SRC as entry files and Shadow DOM structure description files respectively:

<! -- template.html -->
<span class="wrapper">
  <span class="icon" tabindex="0"></span>
  <span class="info"></span>
</span>
Copy the code
// index.js
import html from './template.html';

const template = document.createElement('template');
template.innerHTML = html

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('popup-info', PopUpInfo);
Copy the code

This separates the structure and gives the editor syntax highlights. Add the packaging script to scripts in package.json:

"build": "webpack --config webpack.config.js"
Copy the code

Run NPM run build in the root directory of the project to get the popup-info.js file.

<popup-info></popup-info>
<script src="./dist/popup-info.js"></script>
Copy the code

Check Element:

But that’s just creating a static structure, and for some dynamic behavior, you still need to write code to control it:

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);const shadowRoot = this.attachShadow({ mode: 'open' });
    // Get the content of the attribute and add the content to the info element
    shadowRoot.appendChild(template.content.cloneNode(true));
    const text = this.getAttribute('text');
    shadowRoot.querySelector('.info').textContent = text;
    / / insert icon
    let imgUrl;
    if (this.hasAttribute('img')) {
      imgUrl = this.getAttribute('img');
    } else {
      imgUrl = 'img/default.png';
    }
    const img = document.createElement('img');
    img.src = imgUrl;
    shadowRoot.querySelector('.icon').appendChild(img); }}Copy the code

Modify the element:

<popup-info img="img/alt.png" text="Your card Validation code (CVC) is an extra security feature -- it is the last 3 or 4 numbers on the back of Your card."></popup-info>
Copy the code

Check Element again:

2. Separate styles and use CSS preprocessors

Once you’re done with the structure, look at how the styles separate. As mentioned earlier, to style the Shadow DOM, you need to create the style element and fill it with normal CSS text. In this mode, you can’t use the CSS preprocessor, and the editor can’t highlight its syntax. Referring to the article “Web Components with Shadow DOM and Sass.” [2], Sass is used to write Shadow DOM component styles.

First install raw-loader and sass-loader:

npm install --save-dev raw-loader
npm install --save-dev sass-loader
npm install --save-dev node-sass
Copy the code

Change webpack.config.js to add rule:

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production'.entry: './src/index.js'.output: {
    filename: 'popup-info.js',},module: {
    rules: [{
        test: /\.html$/i,
        loader: 'html-loader'.options: {
          minimize: true.sources: false}}, {test: /\.scss$/,
        use: [
          'raw-loader',
          {
            loader: 'sass-loader'.options: {
              sassOptions: {
                includePaths: [path.resolve(__dirname, 'node_modules'}}}],},};Copy the code

You can then create a separate style file popup-info.scss:

$img-width: 1.2 rem;

.wrapper {
  position: relative;
}

.info {
  font-size: 0.8 rem;
  width: 200px;
  display: inline-block;
  border: 1px solid black;
  padding: 10px;
  background: white;
  border-radius: 10px;
  opacity: 0;
  transition: 0.6 s all;
  position: absolute;
  bottom: 20px;
  left: 10px;
  z-index: 3;
}

img {
  width: $img-width;
}

.icon:hover+.info..icon:focus+.info {
  opacity: 1;
}
Copy the code

Introduce in the entry file:

// index.js
import styleText from './popup-info.scss';

const style = document.createElement('style');
style.appendChild(document.createTextNode(styleText));

class PopUpInfo extends HTMLElement {
  constructor() {
    super(a);const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(style);
    // ...}}Copy the code

Check Element again:

<style>The element has been added and only works inside Shadow DOM. At this point, you’ve written a Shadow DOM component.

thinking

The basic idea of the above process is to separate structure and presentation and reassemble them to improve the maintainability of components to some extent. But the processing of custom element attributes and adding event listeners to the Shadow DOM still needs to be done in the constructor. The separation of DOM structure from CSS styles also separates concerns, requiring switching between different files during development. Referring to Vue SFC, a more reasonable component structure would look like this:

<template>.</template>

<script>.<script>

<style>.</style>
Copy the code

To write Shadow DOM components in this structure, we need to implement a Webpack Loader that can export Shadow DOM components to meet the requirements. We’ll work on it when we have the energy. 🧑 🏻 💻

reference

[1] Using shadow DOM – Web Components | MDN

[2] Web Components with Shadow DOM and Sass.