Search

Learn the Lifecycle of a Web Component by Building a Custom Element

Ashley Parker

Loren Klingman

Megan Mason

Richard Flosi

6 min read

Jan 26, 2022

Learn the Lifecycle of a Web Component by Building a Custom Element

What are Web Components?

Web components are a web standard that was first introduced in 2011 but have not seen much adoption. Adopting web standards creates a future-proof solution because the web is built to be backward compatible. Custom elements are part of the web component ecosystem, and the lifecycle methods of a custom element are key to creating the desired UI experience. The following is an exploration of the available lifecycle methods and how they could be used when creating a custom button component. The button component used in this blog post is part of a larger photo gallery demo. The full code for the photo gallery can be found here. We also have a demo available. Explore the photo gallery code base and demo to learn more about web components.

TL;DR

This is the button that will be built in this post with all available lifecycle methods:

const template = document.createElement("template");
template.innerHTML = `<button id="button">Click ME</button>`;

customElements.define(
  "wc-button",
  class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    connectedCallback() {
      this.addEventListener("click", this.onclick);
    }

    adoptedCallback() {
      console.log(“moved to a new document”);
    }

    disconnectedCallback() {
      this.removeEventListener("click", this.onclick);
    }

    static get observedAttributes() {
      return ["disabled"];
    }

    set disabled(bool) {
      this.setAttribute("disabled", bool.toString());
    }

    get disabled() {
      return this.getAttribute("disabled") === "true";
    }

    attributeChangedCallback(attrName, oldVal, newVal) {
      switch (attrName) {
        case "disabled": {
          this.shadowRoot.getElementById("button").disabled = newVal === "true";
          break;
        }
        default: {
          console.log("unhandled attribute change", attrName, oldVal, newVal);
          break;
        }
      }
    }

    onclick() {
      const button = this.shadowRoot.getElementById("button");
      if (event.composedPath().includes(button)) {
        console.log("button clicked");
      }
    }
  },
);

Defining Custom Elements

In order to register a web component, you must define a Custom Element. To define a custom HTML element, use customElements.define(). This function registers a custom element that extends the HTMLElement interface, a native browser API. customElements.define() takes three parameters: the name of the custom element, the constructor for that element, and an optional options object. As of this writing, the options object only supports a single option called extends which is used to specify the name of a built-in element to extend in order to create a customized built-in element. In this example, the name of the custom element is wc-button and the second parameter is the element class.

customElements.define(
  "wc-button",
  class extends HTMLElement {
    // ...
  }
);

It is important to note that the name of a custom element must include a dash to avoid naming conflicts with any built-in HTML elements. Additionally, custom elements need a closing tag because there are only a few HTML elements that can be self-closing. Custom elements can be imported into HTML files inside a <script type="module"> tag and then used in the same manner as any standard HTML element. Setting the type attribute equal to module is important in order to declare the script as a JavaScript module, and for the component to be imported properly.

<html>
  <head>
    <script type="module">
      import "/button.js";
    </script>
  </head>
  <body>
    <wc-button></wc-button>
  </body>
</html>

constructor()

The constructor is defined within the class to define a custom element. Generically, the constructor is a method used to create and initialize an object instance of that class. In the web component lifecycle, the constructor is the first method of the lifecycle and is called once the web component is initialized. The first method called in the constructor is super, a keyword used to access and call functions on the parent object. super must be called first in order to access this and establish the correct prototype chain. In this case, the parent element being accessed is HTMLElement, a class that provides a standard set of properties, event handlers, methods, and events.

Using the Shadow DOM provides encapsulation of the HTML, CSS, and behavior of the web component keeping it hidden from other elements on the same web page.

const template = document.createElement("template");
template.innerHTML = `<button id="button">Click ME</button>`;

customElements.define(
  "wc-button",
  class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
);

Following super, a shadow root is created and attached to the custom element thereby creating an internal shadow DOM structure. When attaching the custom element to the shadow root, the mode must be set to open. When open, the shadow DOM can be accessed using JavaScript. However, when closed, the shadow DOM cannot be accessed from the outside. Once attached, content can be added to the shadow DOM via this.shadowRoot. For example, here the template content is appended to the shadow root using this.shadowRoot.appendChild().

connectedCallback()

The connectedCallback() method will be called once each time the web component is attached to the DOM. Since the shadow root was attached in the constructor(), then connectedCallback() can be used to access attributes, child elements, or attach event listeners. If the component is moved or removed and re-attached to the DOM, then connectedCallback() will be called again.

customElements.define(
  "wc-button",
  class extends HTMLElement {
    // ...
    connectedCallback() {
      this.addEventListener("click", this.onclick);
    }

    onclick() {
      console.log("clicked handled");
    }
  }
);

In the button example, the connectedCallback() lifecycle method is used to add a click event listener to the component.

attributeChangedCallback()

To use the attributeChangedCallback() method, the attributes to be observed must first be defined in a static method called observedAttributes(). This method returns an array of the attribute names.

Once the attribute has been returned from observedAttributes(), the lifecycle method attributeChangedCallback() will be called every time that attribute is updated. This method has three parameters: the attribute name being changed, the old value of that attribute, and the updated value. Attributes are updated when this.setAttribute() is triggered.

customElements.define(
  "wc-button",
  class extends HTMLElement {
    // ...
    static get observedAttributes() {
      return ["disabled"];
    }
    attributeChangedCallback(attrName, oldVal, newVal) {
      if (attrName === "disabled") {
        this.shadowRoot.getElementById("button").disabled = newVal === "true";
      }
    }
    set disabled(bool) {
      this.setAttribute("disabled", bool.toString());
    }
    get disabled() {
      return this.getAttribute("disabled") === "true";
    }
  }
);
<html>
  <head>
    <script type="module" src="./button.js"></script>
  </head>
  <body>
    <script>
      document.querySelector("wc-button").disabled = true;
    </script>
  </body>
</html>

 

This example watches the custom element’s disabled attribute; when that attribute is changed, the node’s disabled property is also updated.

Attributes are stored as serialized data. getters and setters can be defined on the class to handle serializing and deserializing data on storage and retrieval.

adoptedCallback()

The adoptNode() method is used to move a node from one document to another. This is often used when working with iFrame components. adoptedCallback() is triggered when document.adoptNode() is used to move the web component to a new document.

customElements.define(
  "wc-button",
  class extends HTMLElement {
    // ...
    adoptedCallback() {
      console.log(“moved to a new document”);
    }
  }
);


document.adoptNode(
  document.getElementById("iframe").contentDocument.getElementById("wc-button")
);

disconnectedCallback()

The disconnectedCallback() method will be called when the web component is removed from the DOM.

customElements.define(
  "wc-button",
  class extends HTMLElement {
    // ...
    disconnectedCallback() {
      this.removeEventListener("click", this.onclick);
    }
  }
);
document.body.removeChild(document.getElementById("wc-button"));

In the button example, this lifecycle method is used to clean up and remove the click event listener.

Conclusion

This custom button element can be reused throughout a project. To recap, when building web components, first the custom element must be registered using customElements.define(). Then, once initialized, the constructor() is called to append the node to the shadow DOM. When the element is attached, the connectedCallback() is triggered and is used to attach an event listener. Each time the disabled attribute on the element is updated, the attributeChangedCallback() is triggered. If moved to another document, the adoptedCallback() method will be used. Finally, when removed from the DOM, the disconnectedCallback() method is called to clean up the event listener.

Loren Klingman

Author Big Nerd Ranch

Loren Klingman is a full-stack web developer and instructor at Big Nerd Ranch. He has over 15 years of experience across a variety of tech stacks. When he’s not at work, he can be found playing tabletop games.

Richard Flosi

Author Big Nerd Ranch
Richard Flosi brings 20+ years of web development experience to Big Nerd Ranch. Richard is passionate about using full-stack JavaScript serverless architecture to build scalable long-term solutions.
Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

We are ready to discuss your needs.

Stay in Touch WITH Big Nerd Ranch News