Custom Elements

Custom HTML elements can be created by a class with its properties and methods, events, and more.

When a custom element is specified, it can be used in parallel with built-in HTML elements.

Two kinds of custom elements can be distinguished:

  • Autonomous: to this group are related the “all-new” elements that extend the HTMLElement class.
  • Customized built-in elements: these are the ones that extend built-in elements such as a customized button built on the HTMLButtonElement.

We will start at discovering the autonomous custom elements. For generating a custom element, it’s necessary to inform the browser about several details about it: how to act when the element is added or removed, how to demonstrate it, and so on.

You can do it by generating a class with specific methods.

The sketch containing the full list will look like this:

class CustomElement extends HTMLElement {
  constructor() {
    super();
    // element created
  }
  connectedCallback() {
    //the browser calls this method when an element is added to the document
    // (it can be called many times if an element is added/removed many times)
  }
  disconnectedCallback() {
    // the browser calls this method, when the element is removed from the document
    // (it can be called many times if an element is added/removed many times)
  }
  static get observedAttributes() {
    return [ /*array of attribute names for change tracking*/ ];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    // called when one of the listed attributes is changed
  }
  adoptedCallback() {
    // called once the element is transferred to a new document
    //occurs in document.adoptNode, applied very seldom
  }
  //  there may be other methods and properties of the element
}

Then, it’s required to register the element:

// tell the browser that <we-element> is served by our new class
customElements.define("custom-element", CustomElement);

For each HTML element with <custom-element> tag, a CustomElement instance is generated, and the methods above are called.

In JavaScript, it is also possible to call document.createElement('custom-element').

It’s essential to know that the name of a custom element must contain a hyphen - (for example, custom֊element))). To be more precise let’s consider an example of creating <time-formatted> element.

In HTML, there is a <time> element, which doesn’t do formatting itself. So, the element will display the time in a language-aware format, like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <!-- (3) -->
    <time-format datetime="2020-04-11" year="numeric" month="long" day="numeric" hour="numeric" minute="numeric" second="numeric" time-zone-name="short"></time-format>
    <script>
      class TimeFormat 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-format", TimeFormat); // (2)
    </script>
  </body>
</html>

May 11, 2020, 4:00:00 AM GMT+4

  1. The class includes a single connectedCallback() method. The browser will call it once the <time-formatted> element is attached to the page. It applies the built-in Intl.DateTimeFormat, supported by browsers for demonstrating a proper-formatted time.
  2. It is required to register the new element by customElements.define(tag, class).
  3. Then, it can be used everywhere.

Upgrading Custom Elements

In case the browser faces-off the <time-formatted> elements before customElements.define. That’s not considered an error. But, the element is not revealed yet.

However, you can style undefined elements with CSS selector: not(:defined). For getting information about custom elements, the methods below are used: customElements.get(name): returning the class for a custom element with a specific name.

customElements.whenDefined(name): returning a promise, which resolves once a custom element with a specific name is defined.

Rendering

It’s important to note that rendering should take place in connectedCallback and not in the constructor.

The connectedCallback occurs when the element is inserted into a document: not just appended but becomes a part of the page. So, a detached DOM can be built.

Observing Attributes

In the course of the <time-formatted> implementations, after the rendering of the element, further attribute changes will not be efficient.

As a rule, while changing an attribute such as a.href, the change is expected to be visible at once.

The attributes can be observed through a list in observedAttributes(). The attributeChangedCallback is called once they are changed.

Let’s see a new <time-formatted> capable of auto-updating when attributes are modified:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <time-format id="elem" hour="numeric" minute="numeric" second="numeric"></time-format>
    <script>
      class TimeFormatted extends HTMLElement {
        render() { //  ( 1 )
          let dateTime = new Date(this.getAttribute('datetime') || Date.now());
          this.innerHTML = new Intl.DateTimeFormat("default", {
              y: this.getAttribute('year') || undefined,
              m: this.getAttribute('month') || undefined,
              d: this.getAttribute('day') || undefined,
              h: this.getAttribute('hour') || undefined,
              m: this.getAttribute('minute') || undefined,
              s: this.getAttribute('second') || undefined,
              timeZoneName: this.getAttribute('time-zone-name') || undefined,
            }).format(dateTime);
        }
        connectedCallback() { // (2)
          if(!this.rendered) {
            this.render();
            this.rendered = true;
          }
        }
        static get observedAttributes() { // (3)
          return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
        }
        attributeChangedCallback(name, oldValue, newValue) { // (4)
          this.render();
        }
      }
      customElements.define("time-format", TimeFormatted);
      setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
      customElements.define("time-formatted", TimeFormatted); // (2)
    </script>
  </body>
</html>

So, let’s clarify what happens here:

  1. The logic of the rendering is transferred to the render() helper method.
  2. It is called once when the element is put into the page.
  3. For an attribute modification, listed inside observedAttributes(), attributeChangedCallback occurs.
  4. Then, the element is re-rendered.
  5. And, a live-timer is created at the end.

Rendering Order

Once HTML parser develops the DOM, the elements are progressed one after another. For example, when there is <outer><inner></inner></outer>, then the <outer> element is generated and linked to DOM first, and <inner>, afterward. It brings crucial consequences for custom elements.

When a custom element attempts to access innerHTML in connectedCallback, it will not get anything:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <script>
      customElements.define('site-info', class extends HTMLElement {
        connectedCallback() {
          alert(this.innerHTML); // empty
        }
      });
    </script>
    <site-info>W3Docs</site-info>
  </body>
</html>

If running it, the alert is empty.

The reason is that no children exist on that stage, and the DOM is not finished. In case you intend to pass information to the custom element, attributes may be used. And, in case the children are needed, access can be deferred to them using the zero-delay setTimeout.

It will operate as follows:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <script>
      customElements.define('site-info', class extends HTMLElement {
        connectedCallback() {
          setTimeout(() => alert(this.innerHTML)); // W3Docs
        }
      });
    </script>
    <site-info>W3Docs</site-info>
  </body>
</html>

However, the solution above is not perfect. In case the custom elements also use setTimeout for initializing themselves, then in the queue, the setTimeout will occur first, and the inner one will follow it.

So, the inner element finishes initialization after the outer one.

Here is an example:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <script>
      customElements.define('site-info', class extends HTMLElement {
        connectedCallback() {
          alert(`${this.id} connected.`);
          setTimeout(() => alert(`${this.id} initialized.`));
        }
      });
    </script>
    <site-info id="outer">
      <site-info id="inner"></site-info>
    </site-info>
  </body>
</html>

The output order looks like this:

  1. outer connected.
  2. inner connected.
  3. outer initialized.
  4. inner initialized.

Customized Built-in Elements

Built-in HTML elements can be extended and customized by inheriting from their classes.

For instance, let’s try to extend HTMLButtonElement:

class WelcomeButton extends HTMLButtonElement { /* custom element methods */ }

Then, it’s necessary to provide a third argument customElements.define, indicating the tag, like this:

customElements.define('hello-button', WelcomeButton, {
  extends: 'button'
});

Different tags sharing the same DOM-class can exist. That’s why extends is necessary.

And, finally, it’s required to insert a regular <button> tag and insert is="welcome-button" to it as follows:

<button is="welcome-button">...</button>

The full example will look like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the document</title>
  </head>
  <body>
    <script>
      // The button that says "welcome" on click
      class WelcomeButton extends HTMLButtonElement {
        constructor() {
          super();
          this.addEventListener('click', () => alert("welcome"));
        }
      }
      customElements.define('welcome-button', WelcomeButton, {
        extends: 'button'
      });
    </script>
    <button is="welcome-button">Click me</button>
    <button is="welcome-button" disabled>Disabled</button>
  </body>
</html>

Summary

In this chapter, we discovered two types of custom elements: Autonomous and Customized built-in elements.

Autonomous custom elements are “all-new” elements that extend the abstract HTMLElement class.

The customized built-in elements are the extensions of the existing elements. Among browsers, the custom elements are well-supported.

Practice Your Knowledge

What are the key steps in creating and using custom elements in JavaScript?

Quiz Time: Test Your Skills!

Ready to challenge what you've learned? Dive into our interactive quizzes for a deeper understanding and a fun way to reinforce your knowledge.

Do you find this helpful?