March 26, 2021

Custom elements

We can create custom HTML elements, described by our class, with its own methods and properties, events and so on.

Once a custom element is defined, we can use it on par with built-in HTML elements.

That’s great, as HTML dictionary is rich, but not infinite. There are no <easy-tabs>, <sliding-carousel>, <beautiful-upload>… Just think of any other tag we might need.

We can define them with a special class, and then use as if they were always a part of HTML.

There are two kinds of custom elements:

  1. Autonomous custom elements – “all-new” elements, extending the abstract HTMLElement class.
  2. Customized built-in elements – extending built-in elements, like a customized button, based on HTMLButtonElement etc.

First we’ll cover autonomous elements, and then move to customized built-in ones.

To create a custom element, we need to tell the browser several details about it: how to show it, what to do when the element is added or removed to page, etc.

That’s done by making a class with special methods. That’s easy, as there are only few methods, and all of them are optional.

Here’s a sketch with the full list:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // element created
  }

  connectedCallback() {
    // browser calls this method when the element is added to the document
    // (can be called many times if an element is repeatedly added/removed)
  }

  disconnectedCallback() {
    // browser calls this method when the element is removed from the document
    // (can be called many times if an element is repeatedly added/removed)
  }

  static get observedAttributes() {
    return [/* array of attribute names to monitor for changes */];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // called when one of attributes listed above is modified
  }

  adoptedCallback() {
    // called when the element is moved to a new document
    // (happens in document.adoptNode, very rarely used)
  }

  // there can be other element methods and properties
}

After that, we need to register the element:

// let the browser know that <my-element> is served by our new class
customElements.define("my-element", MyElement);

Now for any HTML elements with tag <my-element>, an instance of MyElement is created, and the aforementioned methods are called. We also can document.createElement('my-element') in JavaScript.

Custom element name must contain a hyphen -

Custom element name must have a hyphen -, e.g. my-element and super-button are valid names, but myelement is not.

That’s to ensure that there are no name conflicts between built-in and custom HTML elements.

Example: “time-formatted”

For example, there already exists <time> element in HTML, for date/time. But it doesn’t do any formatting by itself.

Let’s create <time-formatted> element that displays the time in a nice, language-aware format:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>
  1. The class has only one method connectedCallback() – the browser calls it when <time-formatted> element is added to page (or when HTML parser detects it), and it uses the built-in Intl.DateTimeFormat data formatter, well-supported across the browsers, to show a nicely formatted time.
  2. We need to register our new element by customElements.define(tag, class).
  3. And then we can use it everywhere.
Custom elements upgrade

If the browser encounters any <time-formatted> elements before customElements.define, that’s not an error. But the element is yet unknown, just like any non-standard tag.

Such “undefined” elements can be styled with CSS selector :not(:defined).