Web Components give us a way of creating our own HTML elements and using them as if they were native HTML elements themselves. Learning how to create and use web components can potentially save you much time and trouble.
Let's walk through the process of creating a web copmonent, and discuss the knowledge required to do so, using a real example. The contents below will outline the individual pieces, then the code at the end will put it all together. If you'd like to skip the explanation and get directly to the code, feel free to do so by scrolling to the bottom.
What is a Web Component?
A web component is a custom user-built component which can be used as if it were a native HTML element. This enables us to, amongst other things:
- Directly place web components in HTML documents using their own custom selectors.
- Specify input properties directly within the HTML markup.
- Add event listeners directly to the component references.
Here, we're going to walk through the process of creating a custom web component by creating a countdown timer which will display how much time remains until a certain target timestamp is reached.
Example Component Requirements
Before continuing, let's outline what exactly our custom component we'll be making for this example should do. It should:
- Accept a target timestamp as an input property targetTimestamp.
- Display the amount of time remaining in a text format such as: 1 year, 3 months, 10 days.
- Display target elapsed if the target timestamp is in the past.
- Update once per second so the user can see the countdown in action.
How to Create / Define a Web Component
To create a web component, we will need to:
- Define how the class should render its contents and respond to input changes.
There are multiple ways we can have a compoent render its contents and CSS styles, each with its own advantages and disadvantages. We'll cover those in more detail in the sections below.
Creating the Component Class
Let's start off our implementation by creating the class for our custom component:
Let's add the core functionality we'll need to satisfy the requirements above. In the code below, we've added logic to:
- Create the string which formats the time until the target timestamp.
- Update the display once per second.
There are still a few things missing:
- Respond to input property changes.
- Initially create the child components when the element is attached to the document
- Dipose once the element is disconnected.
There are a few lifecycle hooks we need to integrate. We'll cover those in the sections below.
Web Component Lifecycle
A web component's lifecycle is a description of what happens to that web component throughout an instance's life. A lifecycle hook is a method which will be invoked by the browser when a certain type of event occurs. The following lifecycle hooks apply:
- connectedCallback(): when the component is attached to the document, the browser will invoke this method.
- adoptedCallback(): invoked when this element is moved from one HTML document to another.
- attributeChangedCallback(name, oldValue, newValue): invoked when a property of this component is changed.
- disconectedCallbac(): invoked when the component is disconnected from the DOM.
For this example, we will be using all hooks except for adoptedCallback(). Note that any unneeded lifecycle hooks can be omitted from the component class. We'll cover each hook in more detail in the sections below.
If our component needs to take some action when it is attached to the DOM, we'll need to implement the connectedCallback method in our class. In our countdown timer, we'll need to create the child element which will render the countdown timer string. In the code below, we've implemented the connectedCallback hook within our class and have put logic to create the child component.
Now that we've implemented the method, the browser itself will take care of invoking it when needed; we do not need to take any further action for that lifecycle hook.
If our component needs to take action when it is removed from the DOM, such as resource cleanup, we'll need to implement the disconnectedCallback method in our class. In our countdown timer, we'll need to clear the timer we're using. In the code below, we've implemented that lifecycle hook with that logic:
Attribute Changed Callback
If our component needs to respond to attribute / property changes, we'll need to implement the attributeChangedCallback method. We'll also need to implement a static getter method observedAttributes so the browser knows which attributes we need to listen to. In our countdown timer, we need to listen to a custom attribute target-timestamp, which means we'll need to specify that as an attribute to observe and respond to that when it is updated. For each method:
- observedAttributes will return an array of strings corresponding to which attributes we need to respond to. Note that this must be a static getter.
- attributeChangedCallback will be invoked with parameters name, oldValue, and newValue corresponding to the name of the attribute which has just changed, its previous value, and its new value.
We've implemented these methods below:
Registering the Component
In order to reference the component within the HTML document by its selector, we'll need to register the component with the browser's custom elements registry. The code below registers our countdown timer element and associates it with the selector we want to use for it:
Using our Component in the DOM
Note that we've added the id attribute and can add others as well, such as class, allowing us to apply styles and otherwise work with custom web components as if they were native HTML elements.
Alternative Ways of Defining HTML Contents
In this example, we've created our child components using the
document.createElement function. We only had one child element to create, but the same approach can be taken if multiple children need to be created. However, there are multiple ways we can define the component's elements / HTML contents, and it is worth discussing the alternatives (not mutually exclusive):
- Shadow Dom: instead of creating / appending children directly to the component, create a shadow dom and append there.
- Template: declare the template for this component on the HTML document itself inside of
<template>...</template>tags, then clone that template in and append.
- HTML String: store the template as an HTML string and apply directly.
The same approaches can also be taken for CSS properties. We will not cover those alternative in any detail here, but it is good to know they exist.
Full Code Implementation
Below, we'll showcase the full code contents which brings everything together: