Welcome to wechat public account: Front Reading Room

Web Components allows developers to create tags that enhance existing HTML tags and extend tags written by other developers.

It provides a web standards-based way to write reusable components in small amounts of modular code.

Define a new element

Use Windows. CustomElements. Define can define a new element.

Its first argument is the tag name and its second argument is a class that inherits HTMLElement.

class AppDrawer extends HTMLElement {}

window.customElements.define('app-drawer', AppDrawer);
Copy the code

So how do you use it? You just use it like a normal HTML tag!

<! DOCTYPEhtml>
<html>
  <head>
    <title>Web Components</title>
  </head>
  <body>
    <app-drawer>app-drawer</app-drawer>
    <script>
      class AppDrawer extends HTMLElement {}

      window.customElements.define("app-drawer", AppDrawer);
    </script>
  </body>
</html>
Copy the code

Custom elements are no different from normal HTML elements. An instance of it can be declared in a page and then defined using JS. It can also use features of HTML elements such as event listeners. More on this later.

JS API for custom elements

The custom element function is implemented using ES2015 Class, which inherits HTMLElement, so it has a full DOM API. This also means that the properties and methods in JS become part of the DOM interface, just as we are using JS to create apis for tags.

Example:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open'.' ');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super(a);// Setup a click listener on <app-drawer> itself.
    this.addEventListener('click'.e= > {
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
  }
}

customElements.define('app-drawer', AppDrawer);
Copy the code

In this example, we create an app-drawer element with an open attribute and a toggleDrawer() method.

In the class syntax that defines elements, this refers to the element itself. In this case it can get properties and listen for events. It works with all other DOM apis, such as getting children(this.children), selecting elements (this.querySelectorAll(‘.items’)), and so on.

Custom element naming rules

  1. Dashes (-) must be included to distinguish custom elements from HTML elements and to ensure compatibility (even if HTML adds new tags, there will be no conflicts)

  2. Do not register the same tag twice, otherwise DOMException will be thrown, because this is completely unnecessary.

  3. Self-closing tags are not supported because only some tags are allowed to be self-closing in the HTML specification. So only
    is correct.

Custom Element Reactions (Custom Element Reactions)

The custom element has its lifecycle hooks. The list is as follows:

The name of the trigger
constructor Executed when an instance is created or upgraded, usually to initialize some state, set event listeners, or create a shadow DOM
connectedCallback Triggered when an element is added to the DOM, this is usually the time for data requests and so on
disconnectedCallback Emitted when an element is removed from the DOM, usually doing some cleanup
attributeChangedCallback(attrName, oldVal, newVal) Triggered when a listener attribute (in the observedAttributes attribute list) is added, deleted, updated, or replaced. Also fired when the element is initialized by the parser to create or upgrade the corresponding attribute value
adoptedCallback Triggered when the element is moved to a new document (e.g. Document.adoptnode (el))

Example:

class AppDrawer extends HTMLElement {
  constructor() {
    super(a);// always call super() first in the constructor.
  }
  connectedCallback(){}disconnectedCallback(){}attributeChangedCallback(attrName, oldVal, newVal){}}Copy the code

The callback function is synchronous. For example, when el.setAttribute() is called, attributeChangedCallback() is executed immediately.

These callbacks are not reliable in all cases, such as disconnectedCallback not executing when the user closes the TAB.

JS and HTML attributes

Set HTML attributes using JS

It is common to use JS to set HTML attributes, such as execution in JS

div.id = 'my-id';
div.hidden = true;
Copy the code

The HTML attribute will become

<div id="my-id" hidden>
Copy the code

This is a very useful feature. Let’s say you want to implement styles that change based on the JS state.

In this example, we can toggle the transparency of the app-drawer element and set some properties for it by clicking.

<! DOCTYPEhtml>
<html>
  <head>
    <title>Web Components</title>
  </head>
  <style>
    app-drawer[disabled] {
      opacity: 0.5;
    }

    app-drawer {
      opacity: 1;
    }
  </style>
  <body>
    <app-drawer>app-drawer</app-drawer>
    <script>
      class AppDrawer extends HTMLElement {
        static get observedAttributes() {
          return ["disabled"];
        }

        get disabled() {
          return this.hasAttribute("disabled");
        }

        set disabled(val) {
          if (val) {
            this.setAttribute("disabled"."");
          } else {
            this.removeAttribute("disabled"); }}// Only called for the disabled and open attributes due to observedAttributes
        attributeChangedCallback(name, oldValue, newValue) {
          // When the drawer is disabled, update keyboard/screen reader behavior.
          if (this.disabled) {
            this.setAttribute("tabindex"."1");
            this.setAttribute("aria-disabled"."true");
          } else {
            this.setAttribute("tabindex"."0");
            this.setAttribute("aria-disabled"."false");
          }
          // TODO: also react to the open attribute changing.
        }

        // Can define constructor arguments if you wish.
        constructor() {
          // If you define a constructor, always call super() first!
          // This is specific to CE and required by the spec.
          super(a);// Setup a click listener on <app-drawer> itself.
          this.addEventListener("click".e= > {
            this.toggleDrawer();
          });
        }

        toggleDrawer() {
          this.disabled = Math.random() > 0.5 ? true : false;
        }
      }

      customElements.define("app-drawer", AppDrawer);
    </script>
  </body>
</html>
Copy the code

Elements to upgrade

We learned how to define an element using customElements. The definition goes with the registration, but you can actually register the element before you define it. CustomElements. Define (‘app-drawer’,… That’s ok.

Because browsers treat unknown tags differently.

If you define an element first and then call define(), this is called an element upgrade.

You can use Windows. CustomElements. WhenDefined () method to monitor element is defined at what time.

customElements.whenDefined('app-drawer').then(() = > {
  console.log('app-drawer defined');
});
Copy the code

Here is an example that does something after all the child elements have been upgraded.

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>

<script>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map(socialButton= > {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() = > {
  // All social-button children are ready.
  // do some thing
});
</script>
Copy the code

Elements define

A custom element can manage its own content in its internal code using the DOM API. The element lifecycle also makes this management easier.

Example – Create an element using the default HTML

customElements.define('x-foo-with-markup'.class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "I'm an x-foo-with-markup! "; }... });Copy the code

The page will display as

<x-foo-with-markup>
 <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
Copy the code

Use Shadow DOM to create elements

The Shadow DOM provides an area of the page where elements can be rendered and styled separately. You can even hide the entire app in a TAB.

To use the Shadow DOM, call this. AttachShadow method from constructor.

<! DOCTYPEhtml>
<html>
  <head>
    <title>Web Components</title>
  </head>
  <body>
    <x-foo-shadowdom>
      <p><b>User's</b> custom text</p>
    </x-foo-shadowdom>
    <script>
      let tmpl = document.createElement("template");
      tmpl.innerHTML = ` <style>:host {}</style> <! -- look ma, scoped styles --> <b>I'm in shadow dom! </b> <slot></slot> `;

      customElements.define(
        "x-foo-shadowdom".class extends HTMLElement {
          constructor() {
            super(a);// always call super() first in the constructor.

            // Attach a shadow root to the element.
            let shadowRoot = this.attachShadow({ mode: "open" });
            shadowRoot.appendChild(tmpl.content.cloneNode(true)); }});</script>
  </body>
</html>
Copy the code

Page rendering results

<x-foo-shadowdom>
  #shadow-root
    <b>I'm in shadow dom!</b>
    <slot></slot> <! -- slotted content appears here -->
</x-foo-shadowdom>
Copy the code

use<template>Create the element

Template allows you to easily declare the structure of an element.

Example – Register a Shadow DOM element using template

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template'.class extends HTMLElement {
    constructor() {
      super(a);// always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true)); }... });</script>
Copy the code

There are a few key points from this example:

  1. Define a new tag

  2. Create the Shadow DOM using template

  3. The element DOM is built-in due to the Shadow DOM

  4. Because of the Shadow DOM, the element’s CSS is also built-in, and styles are limited to the elements themselves

Styling custom elements

Even if your element is styled using the Shadow DOM, the style of the element will be affected by the page style.

Page styles are also known as user-defined styles. If it has the same style as Shadow DOM, the custom style takes effect.

Styles undefined elements

Before an element is defined (upgraded), you can style it using the: Not (:defined) pseudo-class.

This default style is also useful, as you can let an element occupy some layout space, even if it is not defined yet.

In the following example, the element takes up some space before it is defined. Of course, when the element is defined, the app-drawer: NOT (:defined) selector is disabled.

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  width: 100vh;
}
Copy the code

Extension elements

The custom element API is useful not only for creating new HTML elements, but also for extending other custom elements and even elements built into the browser.

Extend custom elements

Extending the custom element is done using the class definition that inherits it.

Example – create

inherit from

class FancyDrawer extends AppDrawer {
  constructor() {
    super(a);// always call super() first in the constructor. This also calls the extended class' constructor.
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);
Copy the code

Inherit native HTML elements

If you want to create a more powerful

Such custom elements that inherit from HTML elements are also called customized built-in elements. It not only captures the features of the native element (attributes, methods, accessibility), but also enhances the functionality of the element. There is no better way to write a progressively enhanced Web application than with custom built-in elements.

Note: Custom built-in elements are not supported by all browsers.

Example – < FancyButton >

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(a);// always call super() first in the constructor.
    this.addEventListener('click'.e= > this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend'.e= > div.remove());
  }
}

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

Notice that define() needs to specify which browser tag is inherited. This is necessary because even different tags may inherit the same DOM interface. For example, and < blockQuote > both inherit HTMLQuoteElement.

Custom built-in elements can add the is=”” attribute to native tags.

<! -- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

<script>
  // Custom elements overload createElement() to support the is="" attribute.
  let button = document.createElement('button', {is: 'fancy-button'});
  button.textContent = 'Fancy button! ';
  button.disabled = true;
  document.body.appendChild(button);
</script>
Copy the code

Or use the new operator

let button = new FancyButton();
button.textContent = 'Fancy button! ';
button.disabled = true;
Copy the code

Example – extension

<! -- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

<script>
customElements.define('bigger-img'.class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10); }}, {extends: 'img'});
</script>
Copy the code

Or create an image instance

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15.20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);
Copy the code

Some of the details

Unknown elements vs. undefined elements

HTML is open and flexible. Say you declare that a < randomTagthatDoesNTexist > tag is infallible. This is because the HTML specification allows it, in which the tag is interpreted as HTMLUnknownElement.

So for custom elements, illegal custom element names may be resolved as HTMLElement (or HTMLUnknownElement).

API reference

Elements can be defined through defines defines on global customElements.

define(tagName, constructor, options)

Example:

customElements.define('my-app'.class extends HTMLElement {... }); customElements.define('fancy-button'.class extends HTMLButtonElement {... }, {extends: 'button'});
Copy the code

get(tagName)

Passing in a valid custom element name returns the constructor for that element. Returns undefined if the element is not registered.

example

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();
Copy the code

whenDefined(tagName)

Return a Promise, and resolve will be executed when the element is defined. If the element is already defined, execute resolve immediately. Reject is executed when the tagName passed in is an invalid name.

Example:

customElements.whenDefined('app-drawer').then(() = > {
  console.log('ready! ');
});
Copy the code

History and browser support

history

Chrome 36+ implements the v0 version of the custom element API, using Document.registerElement (not customElements. Define) to define elements. V0 is deprecated.

Browser vendors currently use the current version v1, which uses customElements. Define () to define elements.

Browser support

Chrome 54, Safari 10.1, And Firefox 63 are all in v1, and Edg is in development.

You can use the following code to determine if your browser supports version V1.

const supportsCustomElementsV1 = 'customElements' in window;
Copy the code

Polyfill

Polyfill can be used for version v1 until it is widely supported in browsers.

We recommend using webComponents. Js loader to achieve optimal loading of Web Components polyfill. It uses feature detection and asynchronously loads polyfills only when needed.

Polyfill installation

npm install --save @webcomponents/webcomponentsjs
Copy the code

Use polyfill

<! -- Use the custom element on the page. -->
<my-element></my-element>

<! -- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<! -- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  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);
    });
  }

  WebComponents.waitFor(() = > {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>
Copy the code

Note: Defined CSS pseudo-classes cannot be polyfilled.

conclusion

Custom elements allow us to define new HTML tags and create reusable components. Combined with other features like Shadow DOM,

It can:

  1. Create and extend reusable components across browsers (Web standards)

  2. No library or framework development required (powerful JS/HTML)

  3. Familiar programming model (DOM/CSS/HTML)

  4. Perfect integration with other new Web platform features ((Shadow DOM,

  5. Tight integration with browser DevTools

  6. Use existing accessibility features.

Translated from

  • Reusable Web Components By Eric Bidelman

Engineer @ Google working on web tooling: Headless Chrome, Puppeteer, Lighthouse

Welcome to wechat public account: Front Reading Room