Components are the direction of front-end development, and the popular React and Vue are both component frameworks.

Google has been pushing for native components for browsers due to its mastery of the Chrome browser, namely the Web Components API. Compared to third-party frameworks, native components are simple and straightforward, intuitive, don’t have to load any external modules, and have a small amount of code. It is still evolving, but is already available for production environments.

The Web Components API has a lot of content, this article is not a comprehensive tutorial, just a simple demonstration to show how to use it to develop components.

1. Custom Elements

The following figure shows a user card

This article demonstrates how to take this card and write it as a Web Components component, and here is the final complete code.

The web page will display the user card as soon as the following code is inserted.

1
<user-card></user-card>

This custom HTML tag is called a custom element. According to the specification, the name of the custom element must contain a hyphen to distinguish it from the native HTML element. So, <user-card> cannot be written as <usercard>.

2. customElements.define()

Custom elements require a class to be defined using JavaScript, and all <user-card>s will be instances of this class.

1
2
3
4
5
class UserCard extends HTMLElement {
  constructor() {
    super();
  }
}

In the above code, UserCard is the class of the custom element. Note that the parent class of this class is HTMLElement, and therefore inherits the features of the HTML element.

Next, use the browser’s native customElements.define() method to tell the browser that the <user-card> element is associated with this class.

1
window.customElements.define('user-card', UserCard);

3. Customizing the content of an element

The custom element <user-card> is currently empty, and the content of this element is given below in the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UserCard extends HTMLElement {
  constructor() {
    super();

    var image = document.createElement('img');
    image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
    image.classList.add('image');

    var container = document.createElement('div');
    container.classList.add('container');

    var name = document.createElement('p');
    name.classList.add('name');
    name.innerText = 'User Name';

    var email = document.createElement('p');
    email.classList.add('email');
    email.innerText = 'yourmail@some-email.com';

    var button = document.createElement('button');
    button.classList.add('button');
    button.innerText = 'Follow';

    container.append(name, email, button);
    this.append(image, container);
  }
}

In the last line of the above code, the this of this.append() represents the custom element instance.

Once this step is done, the DOM structure inside the custom element has been generated.

<template>标签

Writing the DOM structure from the previous section using JavaScript is cumbersome. The Web Components API provides the <template> tag in which you can define the DOM using HTML.

1
2
3
4
5
6
7
8
<template id="userCardTemplate">
  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">yourmail@some-email.com</p>
    <button class="button">Follow</button>
  </div>
</template>

Then, rewrite the custom element’s class to load <template> for the custom element.

1
2
3
4
5
6
7
8
9
class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    this.appendChild(content);
  }
}  

In the above code, after getting the <template> node, all its children are cloned. This is because there may be multiple instances of the custom element, and this template will be left for other instances to use, so its children cannot be moved directly.

Up to this step, the complete code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<body>
  <user-card></user-card>
  <template>...</template>

  <script>
    class UserCard extends HTMLElement {
      constructor() {
        super();

        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
      }
    }
    window.customElements.define('user-card', UserCard);    
  </script>
</body>

5. Add style

Custom elements don’t have styles yet, you can assign global styles to it, like the following.

1
2
3
4

user-card {
  /* ... */
}

However, the style of the component should be encapsulated with the code, taking effect only for custom elements and not affecting the external global style. So, you can write the styles inside <template>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template id="userCardTemplate">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #d4d4d4;
     border: 1px solid #d5d5d5;
     box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
     border-radius: 3px;
     overflow: hidden;
     padding: 10px;
     box-sizing: border-box;
     font-family: 'Poppins', sans-serif;
   }
   .image {
     flex: 0 0 auto;
     width: 160px;
     height: 160px;
     vertical-align: middle;
     border-radius: 5px;
   }
   .container {
     box-sizing: border-box;
     padding: 20px;
     height: 160px;
   }
   .container > .name {
     font-size: 20px;
     font-weight: 600;
     line-height: 1;
     margin: 0;
     margin-bottom: 5px;
   }
   .container > .email {
     font-size: 12px;
     opacity: 0.75;
     line-height: 1;
     margin: 0;
     margin-bottom: 15px;
   }
   .container > .button {
     padding: 10px 25px;
     font-size: 12px;
     border-radius: 5px;
     text-transform: uppercase;
   }
  </style>

  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">yourmail@some-email.com</p>
    <button class="button">Follow</button>
  </div>
</template>

In the code above, the :host pseudo-class inside the <template> style refers to the custom element itself.

6. Parameters for custom elements

The <user-card> content is now set inside <template>, change it to a parameter for ease of use.

1
2
3
4
5
<user-card
  image="https://semantic-ui.com/images/avatar2/large/kristy.png"
  name="User Name"
  email="yourmail@some-email.com"
></user-card>

The <template> code was revamped accordingly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template id="userCardTemplate">
  <style>...</style>

  <img class="image">
  <div class="container">
    <p class="name"></p>
    <p class="email"></p>
    <button class="button">Follow John</button>
  </div>
</template>

Finally, change the code of the class and add the parameters to the custom element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');
    this.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);    

7. Shadow DOM

We don’t want the user to be able to see the internal code of <user-card>, Web Component allows the internal code to be hidden, this is called Shadow DOM, i.e. this part of the DOM is isolated from the external DOM by default, no internal code can affect the external.

The this.attachShadow() method of a custom element enables the Shadow DOM, see the code below for details.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');

    shadow.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);

In the above code, the parameter { mode: 'closed' } of the this.attachShadow() method means that the Shadow DOM is closed and does not allow external access.

At this point, the Web Component component is complete and the full code can be found here. As you can see, the whole process is still very simple, unlike third-party frameworks that have complex API.

8 Component extensions

The components can be extended on the basis of the previous ones.

8.1 Interaction with users

The user card is a static component, and if you want to interact with the user, it is also very simple to listen to various events inside the class.

1
2
3
4
this.$button = shadow.querySelector('button');
this.$button.addEventListener('click', () => {
  // do something
});

8.2 Packaging of components

In the above example, <template> is placed together with the web code, and it is actually possible to inject <template> into the web page using a script. This way, the JavaScript script and <template> can be encapsulated into a JS file, which becomes a standalone component file. The web page can use the <user-card> component by loading this script.

For more advanced uses of Web Components, you can continue with the following two articles.


Reference https://www.ruanyifeng.com/blog/2019/08/web_components.html