preface

As a front-end engineer, we work with components every day. We may have used many third-party component libraries based on React/Vue, such as Ant Design, elementUI, iView, etc., or secondary development of components based on them, such as business components, UI components, etc. Or maybe the team is strong enough to develop its own component library without relying on a third party. No matter what form, Component development has become a necessary skills in our work, in order to better reusability and maintainability, modular development is the inevitable choice, also it is because the modular development more and more important, a few years ago web standards introduced the concept of web Component, designed to solve the problem of native HTML markup language reusability.

At present, the Vue or React framework also supports the use of Web Component, and the React or Vue API can be dynamically called in the Web Component or page rendering, which provides us developers with greater freedom. This allows for a more flexible and elegant approach to componentized development in increasingly complex projects.

We use Web Components to implement componentization natively without relying on third-party frameworks such as Vue or React, and modern browsers support them reasonably well. We believe that Web Components will become the trend of Component development in the future. So I’m going to take you through the Web Component step by step and use it to implement two common components:

  • Button
  • Modal

You can develop more custom components once you’ve mastered Web Components, so write them down and learn about them.

The body of the

Before starting the main text, I would like to repeat the question that many friends have asked me before: how to balance work and growth in the company?

In fact, I’ve experienced this kind of confusion before, when the company was so busy that I had to write business code and had little time to learn and grow. Sometimes after the project is finished, there are new requirements to deal with, and I feel drained. (For b-end products, it is often difficult to make a choice in product control in order to meet customer needs, because customer is god, so the relationship between engineers and products is delicate ~)

, on the other hand, we can improve the work efficiency to compress work time, because business code always done a little regularity and summary, if the overall architecture design of good, usually done for the first time, almost similar business for a second time “pass”, this piece for the front-end, components and modular system is particularly important; Microservices are a good example for the back end.

So speaking of how to learn and grow, the above two points are the summary of the author’s work in the past three years, hoping to give you inspiration.

Another question is how do you learn new technology quickly? This is an answer that you may understand a little bit at the end of this article.

Well, enough of this nonsense, let’s move on to the Web Component. The author combed the knowledge points into the following mind map:

1. Web Component basics

Web Components mainly consists of three technologies, namely

  • Custom Elements
  • Shadow DOM
  • HTML Templates

They can be used together to create powerful custom elements that can be reused anywhere we like without worrying about code conflicts. I will introduce each of these techniques.

1.1 Custom Elements

Custom Elements, also known as custom tags, are defined primarily through the CustomElementRegistry interface, CustomElementRegistry. Define (name, class, extends) method is used to register a custom element, the method accepts the following parameters:

  • Name Specifies the name of the element to be created. The value must be a string that complies with DOMString standards. Note that the name of a Custom Element cannot be a single word and must have a dash
  • Class The class used to define the behavior of an element
  • Extends Optional, a configuration object containing the extends property that specifies from which built-in element the created element inherits. It can inherit any built-in element.

Specific cases are as follows:

customElements.define(
'word-count'.class WordCount extends HTMLParagraphElement {
  constructor() {
    super(a);// The function code of the element. }}, {extends: 'p' });
Copy the code

The custom Element’s lifecycle callback function is called as follows:

  • ConnectedCallback: Called when a Custom Element is first inserted into the document DOM
  • DisconnectedCallback: Called when a Custom Element is removed from the DOM of the document
  • AdoptedCallback: Called when a Custom Element is moved to a new document
  • AttributeChangedCallback: when a custom element increase, delete and modify their own property, is invoked

You can first understand the use of life cycle functions, which will be used in detail in the following component practice.

1.2 Shadow DOM

The Shadow DOM interface allows you to attach a hidden, separate DOM to an element and allows you to attach a hidden DOM tree to a regular DOM tree: The root node starts with the shadow root node, and below this root node can be any element, just like normal DOM elements. MDN has a detailed sketch for you to understand:

  • Shadow host: A regular DOM node to which the Shadow DOM is attached.
  • Shadow Tree: Shadow DOM Internal DOM tree.
  • Shadow boundary: The place where the Shadow DOM ends and where the regular DOM begins.
  • Shadow root: indicates the root node of Shadow tree

If we wanted to attach a Shadow DOM to a Custom Element, we could add the following implementation to the custom Element constructor:

class Button extends HTMLElement {
  constructor() {
    super(a);let shadow = this.attachShadow({mode: 'open'}); }}Copy the code

After we attach Shadow DOM to an element, we can manipulate it using the DOM APIs as follows:

class Button extends HTMLElement {
  constructor() {
    super(a);let shadow = this.attachShadow({mode: 'open'});
    let para = document.createElement('p'); shadow.appendChild(para); }}Copy the code

We can even insert styles into the Shadow DOM like this:

let style = document.createElement('style');

style.textContent = ` .btn-wrapper { position: relative; } .btn { // ... } `

shadow.appendChild(style);
Copy the code

This is the most basic way to define components. A complete demo is as follows:

class Button extends HTMLElement {
  constructor() {
    super(a);let shadow = this.attachShadow({mode: 'open'});
    let para = document.createElement('p');
    shadow.appendChild(para);
    let style = document.createElement('style');

    style.textContent = ` .btn-wrapper { position: relative; } .btn { // ... } `
    
    shadow.appendChild(style);
  }
}
customElements.define('xu-button', Button);
Copy the code

1.3 HTML Templates

The

<template id="xu_tpl">
 <p>Anecdotal stories front end</p>
</template>
Copy the code

We can get a reference to it in JavaScript and add it to the DOM as follows:

let template = document.getElementById('xu_tpl');
let templateContent = template.content;
document.body.appendChild(templateContent);
Copy the code

As for slot, the use is somewhat similar to vue’s slot, mainly providing a slot mechanism. For example, we define a slot in the template:

<template id="xu_tpl">
  <p><slot name="xu-text">Anecdotal stories slot</slot></p>
</template>
Copy the code

We can use slot like this:

<xu-button>
  <span slot="xu-text">Interesting talk about the front end, let the front end more material!</span>
</xu-button>
Copy the code

After introducing the basic concepts, we started to develop them.

2. Web Component development

Before development, let’s take a look at the implementation:

Button
modal

2.1 Button component implementation

Like any VUE or React component, we must define the boundary and function points of the component before designing the component. The author taught you how to build the component system of the front-end team from 0 to 1 in the previous section.

To insert custom content, we can use template and slot. We define template and slot as follows:

<template id="btn_tpl">
  <slot name="btn-content">button content</slot>
</template>
Copy the code

To customize the Button theme, we can use props to control the user. For example, antD’s Button component supports primary, warning, and other types.

// Button.js
class Button extends HTMLElement {
  constructor() {
    super(a);// Get the template content
    let template = document.getElementById('btn_tpl');
    let templateContent = template.content;

    const shadowRoot = this.attachShadow({ mode: 'open' });
    const btn = document.createElement('button');
    btn.appendChild(templateContent.cloneNode(true));
    btn.setAttribute('class'.'xu-button');
    / / define and get the button type primary | warning | default
    const type = {
      'primary': '#06c'.'warning': 'red'.'default': '#f0f0f0'
    }
    const btnType = this.getAttribute('type') | |'default';
    const btnColor = btnType === 'default' ? '# 888' : '#fff';

    // Create a style
    const style = document.createElement('style');
    // Add styles for shadow Dom
    style.textContent = `
      .xu-button {
        position: relative;
        margin-right: 3px;
        display: inline-block;
        padding: 6px 20px;
        border-radius: 30px;
        background-color: ${type[btnType]};
        color: ${btnColor}; outline: none; border: none; Box-shadow: inset 0 5px 10px rgba(0,0,0,.3); cursor: pointer; } `
    shadowRoot.appendChild(style);
    shadowRoot.appendChild(btn);
  }
}
customElements.define('xu-button', Button);
Copy the code

In the constructor, we define all the functionality that an element instance has. The type of the Button component is set by the type attribute passed in by the user before it is mounted. For custom slots, we can get the content from template.content and insert it into shadowRoot to make it slot capable. Specific use is as follows:

<xu-button type="primary"><span slot="btn-content" id="btn_show">Anecdotal stories button</span></xu-button>
<xu-button type="warning"><span slot="btn-content">Anecdotal stories button</span></xu-button>
<xu-button type="default"><span slot="btn-content">Anecdotal stories button</span></xu-button>
Copy the code

Our Button component has most of the native DOM capabilities, including DOM operations, events, etc. We add click events to our custom Button as shown below:

document.querySelector('#btn_show').addEventListener('click', () => {
    modal.setAttribute('visible'.'true');
}, false)
Copy the code

2.2 Modal component implementation

The implementation of Modal component is similar to Button principle, but the process is a little more complicated, we need to consider the slot of Modal content, control of Modal display and hiding, as shown in the following figure:

First of all, we can pass the title content through props, and the content slot of Modal is as follows:

<template id="modal_tpl">
    <style>
      .modal-content {
        padding: 16px;
        font-size: 14px;
        max-height: 800px;
        overflow: scroll;
      }
    </style>
    <div class="modal-content">
      <slot name="modal-content">modal content</slot>
    </div>
</template>
Copy the code

As you can see from tempalte, we define internal styles, and many complex Web components can be implemented with similar designs. So let’s just define it very briefly.

The next focus is on closing buttons and the logic that controls Modal display and hiding. This logic should be implemented inside the Modal component. It is not possible to control Modal display and hiding by manipulating dom styles externally. So let’s just remember that Modal in an ANTD component or elementUI can control the display and hiding of Modal by passing in a visible attribute, and we can turn Modal off without changing any of the attributes when we click the close button in the upper right corner, so how do we do that? How can we implement this logic inside a Web Component?

We can use the Web Component lifecycle described above to solve this problem. First, for the close button, we can bind an event to make Modal hide by controlling the internal style. If the user changes visible externally, how do we make it automatically show or hide as Visible changes?

We can implement automatic listening by using attributeChangedCallback as a lifecycle function and observedAttributes as a static method. We can listen for changes to the Visible attribute in observedAttributes and automatically trigger attributeChangedCallback if the attribute is modified. The specific code is as follows:

attributeChangedCallback(name, oldValue, newValue) {
    if(oldValue) {
      const childrenNodes = this.shadowRoot.childNodes;
      for(let i = 0; i < childrenNodes.length; i++) {
        if(childrenNodes[i].nodeName === 'DIV' && childrenNodes[i].className === 'wrap') {
          if(newValue === 'true') {
            childrenNodes[i].style.display = 'block';
          }else {
            childrenNodes[i].style.display = 'none';
          }
        }
      }
    }
  }
  // If you need to trigger the attributeChangedCallback() callback after the element attribute changes,
  // You must listen for this property. This can be done by defining the observedAttributes() get function
  static get observedAttributes() {
    return ['visible']; 
  }
Copy the code

The reason why oldValue exists in the above code is that when visible is assigned a value, attributeChangedCallback is also triggered. Therefore, to avoid executing this function for the first time, We control that the update is performed only if oldValue exists.

The Modal component is overcome, and the rest is well implemented. Next is the author’s Modal component, the code is as follows:

class Modal extends HTMLElement {
  constructor() {
    super(a);// Get the template content
    let template = document.getElementById('modal_tpl');
    let templateContent = template.content;

    const shadowRoot = this.attachShadow({ mode: 'open' });
    const wrap = document.createElement('div');
    const modal = document.createElement('div');
    const header = document.createElement('header');
    const btnClose = document.createElement('span');
    const mask = document.createElement('div');
    const footer = document.createElement('footer');
    const btnCancel = document.createElement('xu-button');
    const btnOk = document.createElement('xu-button');

    // wrap
    wrap.setAttribute('class'.'wrap');

    // modal
    modal.setAttribute('class'.'xu-modal');

    // header
    let title = this.getAttribute('title');
    header.textContent = title;
    btnClose.setAttribute('class'.'xu-close');
    btnClose.textContent = 'x';
    header.appendChild(btnClose);
    modal.appendChild(header);

    btnClose.addEventListener('click', () => {
      wrap.style.display = 'none';
    })

    // content
    modal.appendChild(templateContent.cloneNode(true));

    // footer
    btnOk.setAttribute('type'.'primary');
    const slot1 = document.createElement('span');
    slot1.setAttribute('slot'.'btn-content');
    slot1.textContent = 'confirm';
    btnOk.appendChild(slot1);

    const slot2 = document.createElement('span');
    slot2.setAttribute('slot'.'btn-content');
    slot2.textContent = 'cancel';
    btnCancel.appendChild(slot2);

    footer.appendChild(btnCancel);
    footer.appendChild(btnOk);
    modal.appendChild(footer);

    // mask
    mask.setAttribute('class'.'mask');
    wrap.appendChild(mask);
    wrap.appendChild(modal);

    // Create a style
    const style = document.createElement('style');
    const width = this.getAttribute('width');
    const isVisible = this.getAttribute('visible');
    // Add styles for shadow Dom
    style.textContent = `
      .wrap {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        top: 0;
        display: ${isVisible === 'true' ? 'block' : 'none'}} // Ignore some styles
    shadowRoot.appendChild(style);
    shadowRoot.appendChild(wrap);
  }
  connectedCallback(el) {
    console.log('insert dom', el)
  }
  disconnectedCallback() {
    console.log('Custom square element removed from page.');
  }
  adoptedCallback() {
    console.log('Custom square element moved to new page.');
  }
  attributeChangedCallback(name, oldValue, newValue) {
    if(oldValue) {
      const childrenNodes = this.shadowRoot.childNodes;
      for(let i = 0; i < childrenNodes.length; i++) {
        if(childrenNodes[i].nodeName === 'DIV' && childrenNodes[i].className === 'wrap') {
          if(newValue === 'true') {
            childrenNodes[i].style.display = 'block';
          }else {
            childrenNodes[i].style.display = 'none';
          }
        }
      }
    }
  }
  // If you need to trigger the attributeChangedCallback() callback after the element attribute changes,
  // You must listen for this property. This can be done by defining the observedAttributes() get function
  static get observedAttributes() {
    return ['visible']; 
  }
}
customElements.define('xu-modal', Modal);
Copy the code

More detailed code has been uploaded to Github, you can clone to the local run experience, the effect is quite good.

Github address: Web Component Demo

The last

If you want to learn more front-end skills, actual combat and learning routes, welcome to join our technology group in the public account “Interesting Talk front-end” to study and discuss together, explore the frontier of the front-end.

More recommended

  • When the back end throws you 100,000 pieces of data at once, what do you do as a front end engineer?
  • Build an interesting crawler platform based on Apify+ Node + React /vue
  • Summary of several common sorting algorithms and search algorithms necessary for programmers
  • Implementing a CMS full stack project from 0 to 1 based on nodeJS (part 1)
  • Implementing a CMS full stack project from 0 to 1 based on nodeJS
  • Vue and React
  • From zero to one teaches you to develop a component library based on VUE
  • Build front-end team component systems from 0 to 1
  • 8 frequently used custom hooks by hand in 10 minutes