preface

This article will introduce you to Some Web Components and finally get you started with a component encapsulation case study.

What are Web Components

MDN: Web Components is a different set of technologies that allow you to create reusable custom elements whose functionality is encapsulated outside of your code and use them in your Web applications.

Web Components is not a single specification, but a component model composed of three different sets of technical standards designed to improve component packaging and code reuse.

  • Custom Elements – Custom Elements
  • Shadow DOM – Shadow DOM
  • HTML template-HTML Template

Let’s walk through the basics of Web Components and use them with a simple example.

A simple example

Create a template

<body>
  <! Create template -->
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
</body>
Copy the code

This can be thought of as our component content, wrapped in template, where the tag and its internal elements are not visible until we manipulate them in JavaScript and add them to the DOM to make them visible.

In addition to the template tag, there is also a slot tag, which should be familiar to those of us who are used to frameworks. Slots help improve flexibility in development and are used in a similar way as we normally do, so we won’t go into details here.

Class object that defines the component

<body>
  <! - template - >
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  
  <script>
    // Define class objects
    class HelloComponent extends HTMLElement {
      // constructor
      constructor() {
        // The super method must be called first
        super(a);/ / get the < template >
        const template = document.getElementById('my-template');
        // Create a shadow DOM and add template to its child node
        const _shadowRoot = this.attachShadow({ mode: 'open' });
        const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); }}// Customize the tag
    window.customElements.define('hello-component', HelloComponent);
  </script>
</body>
Copy the code

The HelloComponent class inherits from HTMLElement, and we mount the DOM element in the constructor.

After getting the

shadow DOM

Above we created a shadow DOM using the attachShadow method. The shadow DOM, as its name suggests, attaches a hidden, independent DOM to an element.

The mode parameter has two optional values: open and closed.

Open means that you can retrieve the Shadow DOM from JavaScript methods inside the page.

let shadowroot = element.shadowRoot;
Copy the code

In contrast, closed means that shadow DOM cannot be obtained externally, and element.shadowRoot will return null.

Shadow DOM is actually quite close to us. Some built-in tags in HTML include shadow DOM, such as input and video.

In developer Tools, open the Settings panel and check Show User Agent Shadow DOM.

Look at the input element.

As you can see, the input is a bit more than we usually notice. You should now know where the input content and placeholder come from, as well as where the default play buttons for video are hidden.

Custom labels

window.customElements.define('hello-component', HelloComponent)
Copy the code

Window. CustomElements. Define methods of the first parameter is the name of the custom tag, the second parameter is used to define the elements of class object.

Note: The name of a custom tag cannot be a single word and must have a dash.

use

<body>
  <! - template - >
  <template id="my-template">
    <p>hello~<slot name="user"></slot></p>
  </template>
  <script>
    // Define class objects
    class HelloComponent extends HTMLElement {
      // constructor
      constructor() {
        // The super method must be called first
        super(a);/ / get the < template >
        const template = document.getElementById('my-template');
        // Create a shadow DOM and add template to its child node
        const _shadowRoot = this.attachShadow({ mode: 'open' });
        const content = template.content.cloneNode(true) _shadowRoot.appendChild(content); }}// Customize the tag
    window.customElements.define('hello-component', HelloComponent);
  </script>
  <! - use - >
  <hello-component>
    <span slot="user">Zhang SAN</span>
  </hello-component>
</body>
Copy the code

The life cycle

In addition to the constructor, Custom Elements has four lifecycle functions:

  • connectedCallback: called when a Custom Element is first inserted into the document DOM.
  • disconnectedCallback: is called when a Custom Element is removed from the document DOM.
  • adoptedCallbackCalled when a Custom Element is moved to a new document.
  • attributeChangedCallback: Called when a Custom Element adds, deletes, or modifies its attributes.
class HelloComponent extends HTMLElement {
  // constructor
  constructor() {
    super(a); }connectedCallback() {
    console.log('Called when a custom element is first connected to the document DOM')}disconnectedCallback() {
    console.log('Called when a custom element is disconnected from the document DOM')}adoptedCallback() {
    console.log('Called when a custom element is moved to a new document')}attributeChangedCallback() {
    console.log('Called when an attribute of a custom element is added, removed, or changed')}}Copy the code

Real: Encapsulate a commodity card component

How we expect to use:

<body>
  <! - introduction -- - >
  <script type="module">
    import './goods-card.js'
  </script>
  <! - use - >
  <goods-card
    img="FM = https://img1.baidu.com/it/u=2613325730, 275475287 & 224 & FMT = auto&gp = 0. JPG"
    goodsName="Running shoes"
  ></goods-card>
</body>
Copy the code

Component content implementation

// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super(a);const template = document.createElement('template')
    template.innerHTML = `  
      

; const _shadowRoot = this.attachShadow({ mode: 'open' }) const content = template.content.cloneNode(true) _shadowRoot.appendChild(content) } } window.customElements.define('goods-card', GoodsCard) Copy the code

Now that we have successfully rendered the DOM, except that the item image and item name are empty, we need to fetch the value passed by the parent component and display it on the view.

class GoodsCard extends HTMLElement {
  constructor() {
    super(a);/ /... Omit some code
    // Get and update the view
    const _goodsNameDom = _shadowRoot.querySelector('.goods-name')
    const _goodsImgDom = _shadowRoot.querySelector('.goods-img')
    _goodsNameDom.innerHTML = this.name
    _goodsImgDom.src = this.img
  }
  
  get name() {
    return this.getAttribute('name')}get img() {
    return this.getAttribute('img')}}Copy the code

At this point we have rendered a commodity card.

Reactive view updates

<goods-card
  img="http://zs-oa.oss-cn-shenzhen.aliyuncs.com/zsoa/goods/v1v2/1021161140018/spu1/349334579590860800.jpg"
  name="Running shoes"
></goods-card>
<script type="module">
  import './goods-card.js'
</script>
<script>
  setTimeout(() = > {
    document.querySelector('goods-card').setAttribute('name'.'Basketball shoes')},2000);
</script>
Copy the code

One problem we see here is that if the component’s pass value changes, the view will not be updated, and the product name will still show running shoes instead of basketball shoes.

In this case, we need to use the previous lifecycle, using attributeChangedCallback to solve the problem and modify the code.

class GoodsCard extends HTMLElement {
  constructor() {
    super(a);/ /... Omit some code
    const _shadowRoot = this.attachShadow({ mode: 'open' })
    const content = template.content.cloneNode(true)
    _shadowRoot.appendChild(content)
    this._goodsNameDom = _shadowRoot.querySelector('.goods-name')
    this._goodsImgDom = _shadowRoot.querySelector('.goods-img')}attributeChangedCallback(key, oldVal, newVal) {
    console.log(key, oldVal, newVal)
    this.render()
  }

  static get observedAttributes() {
    return ['img'.'name']}get name() {
    return this.getAttribute('name')}get img() {
    return this.getAttribute('img')}render() {
    this._goodsNameDom.innerHTML = this.name
    this._goodsImgDom.src = this.img
  }
}
Copy the code

With attributeChangedCallback, we can execute the Render method to update the view when the attribute changes.

There’s also an unfamiliar function called observedAttributes that works with attributeChangedCallback, AttributeChangedCallback does not fire if observedAttributes is not written or if the parameter name is not in the array returned by observedAttributes.

static get observedAttributes() {
  return ['img']}// will not be executed because the commodity name name is not in the array returned by observedAttributes
attributeChangedCallback(key, oldVal, newVal) {
  console.log(key, oldVal, newVal)
  this.render()
}
Copy the code

Event interaction

Finally, add a click event to the button, which is a necessary requirement for component callers to handle their own logic.

constructor() {
  / /... Omit some code
  this._button = _shadowRoot.querySelector('.add-cart-btn')
  this._button.addEventListener('click'.() = > {
    this.dispatchEvent(new CustomEvent('onButton', { detail: 'button'}}})))Copy the code
<body>
  <script>
    document.querySelector('goods-card').addEventListener('onButton'.(e) = > {
      console.log('Add shopping cart', e.detail) // button
    })
  </script>
</body>
Copy the code

The complete code

<body>
  <! - use - >
  <goods-card
    img="FM = https://img1.baidu.com/it/u=2613325730, 275475287 & 224 & FMT = auto&gp = 0. JPG"
    name="Running shoes"
  ></goods-card>

  <! - introduction -- - >
  <script type="module">
    import './goods-card.js'
  </script>
  <script>
    document.querySelector('goods-card').addEventListener('onButton'.(e) = > {
      console.log('Button event', e.detail)
    })
  </script>
</body>
Copy the code
// goods-card.js
class GoodsCard extends HTMLElement {
  constructor() {
    super(a);const template = document.createElement('template')
    template.innerHTML = `  
      

; const _shadowRoot = this.attachShadow({ mode: 'open' }) const content = template.content.cloneNode(true) _shadowRoot.appendChild(content) this._goodsNameDom = _shadowRoot.querySelector('.goods-name') this._goodsImgDom = _shadowRoot.querySelector('.goods-img') this._button = _shadowRoot.querySelector('.add-cart-btn') this._button.addEventListener('click'.() = > { this.dispatchEvent(new CustomEvent('onButton', { detail: 'button'}}})))attributeChangedCallback(key, oldVal, newVal) { console.log(key, oldVal, newVal) this.render() } static get observedAttributes() { return ['img'.'name']}get name() { return this.getAttribute('name')}get img() { return this.getAttribute('img')}render() { this._goodsNameDom.innerHTML = this.name this._goodsImgDom.src = this.img } } window.customElements.define('goods-card', GoodsCard); Copy the code

The last

Above, through the package of a component, I believe you have a certain understanding and understanding of Web Components. Of course, this article only introduces you to some basic development practice knowledge, not the whole Web Components, there are many parts worth in-depth. If you are interested, you can take a look.

Thank you

That’s the end of this sharing, thank you for reading, if you have any help, please feel free to like ❤️.

If there are any mistakes or deficiencies, welcome to comment section correction, exchange ❤️.

References:

Ruan Yifeng – Web Components tutorial

MDN – Web Componnents