All Articles

A Primer on Custom Elements

The Custom Elements API allows us to define our own HTML elements without the need of a JavaScript library or framework. These components can register properties and observe changes to them, they are also performant given that they are processed directly by the browser engine. Here is how to make one.

Defining an Element

Every tag used by the web browser has a class associated with it. For example: button uses HTMLButtonElement, and div uses HTMLDivElement. All of these classes inherit from HTMLElement, and they implement their own functionality. Similarly, we must do the same to create our own HTML tag.

class HTMLCustomElement extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'closed' })
    shadowRoot.innerHTML = `<h1>Hello Custom Element</h1>`;
    this._h1 = shadowRoot.querySelector('h1');
  }
}

We declare a class that extends HTMLElement and attaches a Shadow DOM to it. A Shadow DOM is a parallel DOM that exists within our page, and it is used to achieve isolated HTML and CSS.

When using attachShadow, we must configure its mode to be either open or closed. Closed mode prevents any JavaScript, outside of the Shadow DOM, from performing any manipulations; open allows external changes.

We then pass a template string to the Shadow DOM by setting its innerHTML. We also keep track of any tags within our template which we will use later on, in this case, we are interested in keeping our h1.

Finally, we associate HTMLCustomElement with a tag name, and register it with the browser.

customElement.define('custom-element', HTMLCustomElement);

Our web browser now understands the <custom-element> tag, just like any other HTML tag. However, it is currently unable to receive or register attribute changes. For that, we need callbacks.

Callbacks

A callback is a method within our element that executes based on the usage of the element, these are similar to the lifecycle hooks in React. To use callbacks, we register the attributes that our element uses.

class HTMLCustomElement extends HTMLElement {
  static get observedAttributes() { return ['value']; }
  constructor() { ... }
}

This allows us to pass a value to the value property of custom-element, like so:

<custom-element value="Eric">

However, HTMLCustomElement needs to handle changes as properties pass in. We can do that by implementing attributeChangedCalledback within our class.

attributeChangedCallback(name, oldValue, newValue, namespaceURI) {
  if (name === 'value') {
    // do stuff
  }
}

attributeChangedCallback is invoked whenever any of our registered properties is set or changed. We can use the newValue that comes in to make changes to the Shadow DOM.

attributeChangedCallback(name, oldValue, newValue, namespaceURI) {
  if (name === 'value') {
    this._h1.innerText = newValue;
  }
}

In this case, the value that is passed in (Eric) is used as the new innerText of the h1 tag. Those changes are instantly reflected on the page.

There are also other callbacks available to us such as:

  • connectedCallback
  • disconnectedCallback
  • adoptedCallback(oldDocument, newDocument)

These are invoked whenever the custom element is added, removed, or changed to another document. However, we will not discuss those in this article, refer to the sources for more information.

Conclusion

Could we have written this custom element using a JavaScript library? Absolutely. However, the beauty of using custom elements is that they allow us to use more of the platform, and less third-party libraries.

Sources
Published 19 Nov 2016