For other translations of this series, see [JS Working Mechanism – Xiaobai’s 1991 column – Nuggets (juejin. Cn)] (juejin.cn/column/6988…

Read index: this chapter 5 of this chapter explain the mechanism of custom elements that currently support also is not very perfect, but this part will likely be the basis of the next generation of the front frame modularization, hope that the front end students can have a look, and think compared with the front end of the current framework, native of componentization has what kind of advantage.

An overview of

Earlier we discussed web components as part of the big picture, such as the shadow DOM. The whole goal of the Web Component standard is to extend HTML’s built-in capabilities by creating granular, modular, and reusable elements. Although it is a new W3C standard, it is already supported by major browsers and can be used in production environments with the help of Polyfill

Browsers already give us tools for building applications and websites, such as HTML, CSS and JS. HTML builds, CSS makes it look good, and JS gives it interactive behavior. However, prior to the introduction of Web components, associating JS behavior with HTML structure was not easy.

In this chapter we discuss the basics of Web components, custom elements. Custom elements allow you to create HTML elements with JS logic and CSS styles. Many people confuse shadow DOM with custom elements. But they are different and not interchangeable.

Some frameworks (e.g. Angular, React) try to address these issues in their own way. You can use custom element categories like an Angular directive or a React component. However, custom elements are supported by browsers natively, so you don’t need anything other than JS, HTML, and CSS. However, that doesn’t mean it’s necessary to replace a typical JavaScript framework. Modern frameworks don’t just provide developers with the ability to mimic the behavior of custom elements. Therefore, you can use both frames and custom elements. API

Let’s take a quick look at the API. The target object of customElements has these methods:

  • define(tagName, constructor, options)– Define a new custom element. There are three parameters, the tag name of the custom element, the class definition of the custom element, and an option parameter object. The current option supports one that specifies the name of the HTML built-in element you want to extendextendsA string. To create a custom built – in element.
  • get(tagName)– Returns the structure of a custom element, undefined if the element is not defined. There is only one argument: a valid tag name for the custom element.
  • whenDefined(tagName)— Returns a promise that will be resolved once the custom element is defined. If the element is already defined, it is resolved immediately. If the tag name does not correspond to a valid custom element, the promise is rejected. There is only one argument: a valid tag name for a custom element.

How do I create a custom element

Creating a custom element is too easy. Just do two things: create an element class definition that inherits from the HTMLElement class and give the element a name.

class MyCustomElement extends HTMLElement { constructor() { super(); / /... } / /... } customElements.define('my-custom-element', MyCustomElement);Copy the code

Anonymous classes can also be used to prevent messing up the current scope

customElements.define('my-custom-element', class extends HTMLElement { constructor() { super(); / /... } / /... });Copy the code

What problems do custom elements solve

What’s the problem? Div soups, for example, is a very common construct in modern applications if you have many nested Div elements.

<div class="top-container"> <div class="middle-container"> <div class="inside-container"> <div Class ="inside-inside-container"> <div class=" are-we-rey-doing-this "> <div class=" Mariana-trench ">... </div> </div> </div> </div> </div> </div>Copy the code

This structure is needed, but difficult to read and maintain.

Suppose we need a component like this:



Using traditional HTML it looks like this:

<div class="primary-toolbar toolbar"> <div class="toolbar"> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-undo">&nbsp; </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-redo">&nbsp; </div> </div> </div> </div> </div> <div class="toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-print">&nbsp; </div> </div> </div> </div> </div> <div class="toolbar-toggle-button toolbar-button"> <div class="toolbar-button-outer-box"> <div class="toolbar-button-inner-box"> <div class="icon"> <div class="icon-paint-format">&nbsp; </div> </div> </div> </div> </div> </div> </div>Copy the code

But if we can do this:

<primary-toolbar>
  <toolbar-group>
    <toolbar-button class="icon-undo"></toolbar-button>
    <toolbar-button class="icon-redo"></toolbar-button>
    <toolbar-button class="icon-print"></toolbar-button>
    <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button>
  </toolbar-group>
</primary-toolbar>
Copy the code

Obviously better. Readability and maintainability are improved.

Another problem is reuse. We don’t just write code, we maintain code. One way to improve maintainability is to reuse code rather than writing it over and over again.

Just look at a simple example:

<div class="my-custom-element">
  <input type="text" class="email" />
  <button class="submit"></button>
</div>
Copy the code

Let’s say we use this code over and over again elsewhere in HTML. Now we need to make some changes to each of these elements, so we have to look it up again and again and make the same changes. Wouldn’t it be better to use the following code?

<my-custom-element></my-custom-element>
Copy the code

Modern Web applications are not just static HTML, you need to interact with the application, and this is done with JS. In general, what you need to do is create some elements and then listen for events on them in response to user input. Click, drag or hover events and so on.

var myDiv = document.querySelector('.my-custom-element');

myDiv.addEventListener('click', () => {
  myDiv.innerHTML = '<b> I have been clicked </b>';
});
Copy the code
<div class="my-custom-element">
  I have not been clicked yet.
</div>
Copy the code

Using the custom element API, all logic can be encapsulated inside the element. The following code can do the same as above:

class MyCustomElement extends HTMLElement {
  constructor() {
    super();

    var self = this;

    self.addEventListener('click', () => {
      self.innerHTML = '<b> I have been clicked </b>';
    });
  }
}

customElements.define('my-custom-element', MyCustomElement);
Copy the code
<my-custom-element>
  I have not been clicked yet
</my-custom-element>
Copy the code

At first glance, custom elements seem to require more JS code. In practice, however, it is rare to create a single component that does not need to be reused. Also, in modern applications, many elements are created dynamically. Developers then need to deal with adding elements dynamically using JavaScript or using predefined content in HTML structures, respectively. All of this can be done using custom elements. Overall, custom elements make your code easier to understand and maintain, and lead to smaller, reusable, packaged modules.

requirements

Before you create your first custom element, there are a few rules to follow.

  • Element names must contain one (-). This way the HTML parser knows which element is custom and which is built-in. It also ensures that it does not conflict with the name of the built-in element, for example<my-custom-element>Is effective, while<myCustomElement> 和 <my_custom_element>Is invalid
  • You cannot register the same label name twice. This will cause aDOMException. You can’t override custom elements either
  • Custom elements cannot be self-enclosed. The HTML parser only allows built-in elements to be self-enclosed (e.g<img>.<link>.<br>)

Ability to

What can a custom element do? The answer is: a lot. The best feature is that the element’s class defines the DOM element that it actually points to, so you can use this to directly add event listeners, attribute access, child node access, and so on

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    this.addEventListener('mouseover', () => {
      console.log('I have been hovered');
    });
  }

  // ...
}
Copy the code

Naturally, this gives you the ability to override the element’s child nodes. This is not recommended, however, as it may lead to unexpected behavior. The markup inside the element is replaced by something else, which makes the user feel strange.

Developers can execute code in lifecycle hooks at certain stages of an element’s life cycle.

constructor

It is called when the element is created or upgraded. This phase is typically used to initialize the state, add event listeners, create shadow DOM, and so on. Remember, always call super()

connectedCallback

Called when the element is added to the DOM. This phase can delay executing something until the element is actually loaded on the page (such as fetching resources)

disconnectedCallback

Called when an element is unloaded from the DOM. This phase is usually used to release resources. Keep in mind that this method will never be called if the user closes TAB. So be careful when initializing at first. attributeChangedCallback

This method is called whenever an element adds, deletes, updates, or replaces an attribute. It is also called when the parser is created. Note, however, that only attributes in the observedAttributes attribute whitelist are triggered.

addoptedCallback

Call document. AdoptNode (…). This method is then triggered to move the element to a different document.

All of the above callbacks are synchronized. For example, only the connection callback is triggered when an element is added to the DOM.

Attribute reflection

The built-in element provides a nice capability: attribute reflection. This means that the values of these attributes can be reflected directly into DOM attributes, such as id

myDiv.id = 'new-id';

The DOM will be updated

<div id="new-id"> ... </div>

You can do it the other way around. This allows you to configure elements declaratively

Custom elements do not gain this capability, but have a way to implement it themselves. We can define getters and setters for element attributes

class MyCustomElement extends HTMLElement { // ... get myProperty() { return this.hasAttribute('my-property'); } set myProperty(newValue) { if (newValue) { this.setAttribute('my-property', newValue); } else { this.removeAttribute('my-property'); }} / /... }Copy the code

Extension elements

The custom element API not only allows you to create a new HTML element, but also allows you to extend an existing element. And the interface works well with built-in elements and other custom elements. Simply extend the element’s class definition.

class MyAwesomeButton extends MyButton {
  // ...
}

customElements.define('my-awesome-button', MyAwesomeButton);
Copy the code

Or, when extending built-in elements, customElements. Define (…) The extends function adds a third extends parameter whose value is the tag name of the element that you want to extend. Since many built-in elements share the same DOM interface, the extends parameter tells the browser which target element to extend. If you do not specify the element to be extended, the browser will not know the category of functionality to be extended.

class MyButton extends HTMLButtonElement {
  // ...
}

customElements.define('my-button', MyButton, {extends: 'button'});
Copy the code

Extensions of native elements, also known as custom built-in elements. A recommended rule is to extend existing elements as much as possible. Then, little by little, add features to it. This way you can keep all the previous features (properties, methods, etc.)

Custom built-in elements are now only supported in Chrome 67+. It will be implemented by other browsers besides Safari.

Upgrade elements

We use customElements. Define (…) Method to register a custom element. But that’s not the first thing we have to do.

Registering custom elements can be deferred. Even if the element is added to the DOM. This process is called the upgrade of elements. The browser provides customElements. WhenDefined (…). Method to let the developer know exactly what elements are defined. This method passes the tag name of the element. It then returns a promise, which is resolved when the element is registered.

customElements.whenDefined('my-custom-element').then(_ => {
  console.log('My custom element is defined');
});
Copy the code

For example. You might want to defer doing something until all the child elements are defined. This is useful when you have nested elements. Implementations of dominant parent elements or dependent child nodes. In this scenario, you need to make sure that the child element is defined before the parent element.

The shadow of the DOM

As mentioned earlier, custom elements are different from shadow DOM. The former is used to encapsulate JS logic, while the latter is used to create an isolated environment for a small section of DOM that is not affected by external influences. To use the shadow DOM in a custom element, call this.attachShadow

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    let shadowRoot = this.attachShadow({mode: 'open'});
    let elementContent = document.createElement('div');
    shadowRoot.appendChild(elementContent);
  }

  // ...
});
Copy the code

Templates

Templates have been discussed before. Here’s a quick look at using templates when creating your own custom elements. With

<template id="my-custom-element-template">
  <div class="my-custom-element">
    <input type="text" class="email" />
    <button class="submit"></button>
  </div>
</template>
Copy the code
let myCustomElementTemplate = document.querySelector('#my-custom-element-template');

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(myCustomElementTemplate.content.cloneNode(true));
  }

  // ...
});
Copy the code

We have now combined custom elements, shadow DOM, and templates. The new element scope we created is isolated from other elements and perfectly separates HTML structure from JavaScript logic.

styling

How do we style our elements externally? It’s simple —- just like styling built-in elements

my-custom-element {
  border-radius: 5px;
  width: 30%;
  height: 50%;
  // ...
}
Copy the code

Note that the styles we define externally have higher permissions, overwriting the styles inside the element.

Developers need to understand that sometimes pages are rendered and then at some point unstyled content flickers (FOUC) are found. Developers can define styles for undefined components and use animated transitions when elements are defined. Use the :defined selector to achieve this effect.

my-button:not(:defined) {
  height: 20px;
  width: 50px;
  opacity: 0;
}
Copy the code

Unknown elements and undefined custom elements

HTML is quite flexible, running all kinds of tags you declare. If the browser does not recognize the tag, it will parse it as HTMLUnknownElement

var element = document.createElement('thisElementIsUnknown');

if (element instanceof HTMLUnknownElement) {
  console.log('The selected element is unknown');
}
Copy the code

However, custom elements do not. Remember the naming conventions for custom elements we discussed earlier? If the browser sees a valid custom element name, it will parse it as HTMLElement, and the browser will treat it as an undefined custom element.

var element = document.createElement('this-element-is-undefined');

if (element instanceof HTMLElement) {
  console.log('The selected element is undefined but not unknown');
}
Copy the code

It seems that HTMLElement is no different from HTMLUnknownElement. But parsers treat them differently. A valid custom element has a custom implementation that is treated as an empty DIV element until the implementation details are defined. An undefined element does not implement any of the built-in elements’ methods or attributes.

Browser support

Custom elements are now available in version V1 and have been supported since Chrome 54 and Safari 10.1. But only some WebKit browsers are fully supported. Fortunately, there are some profill that can help, even on IE11

Availability check

Check the presence of the customElements attribute in the window object to determine whether the browser supports customElements.

const supportsCustomElements = 'customElements' in window; If (supportsCustomElements) {// Custom element interface can be used}Copy the code

Otherwise use the ProlyFill library:

function loadScript(src) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } // Lazy load the polyfill if necessary. If (supportsCustomElements) {// Browser native support for custom elements} else { LoadScript ('path/to/custom-elements.min.js').then(_ => {// load custom element shippers}); }Copy the code

In summary, the custom elements in the Web Component standard provide developers with the following capabilities:

  • Associate HTML elements with JS and CSS
  • Extend existing HTML elements (both built-in and custom)
  • No additional libraries or frameworks are needed to get started, at most a few polyfill libraries to enable the browser to support custom elements.
  • Easy to use with other Web components (shadow DOM, templates, slots, etc.)

– Tightly integrated with browser developer tools.

  • Use known accessibility features