Now Available React Programming: The Big Nerd Ranch Guide
Front-End ReactBased on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
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.
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"); } } }, );
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>
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()
.
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.
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.
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") );
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.
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.
Based on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
Svelte is a great front-end Javascript framework that offers a unique approach to the complexity of front-end systems. It claims to differentiate itself from...