Learn basic Web Components
Lenvin Gonsalves
Posted on April 20, 2021
Even though web components have been losing steam in recent days, they have a lot of advantages. One of them is writing framework-agnostic components, which is a boon provided how frequently frameworks lose their popularity in js land.
Many organizations have projects with front-ends using different frameworks, and by writing basic components like buttons, modals as web components, we can greatly increase code re-usability. Web components are not here to replace frameworks like React, Vue, and Angular, but are to be used along with frameworks
Using web components also allows encapsulating the styling to the component (Using the shadow DOM), which helps a lot in larger projects where we need to be careful about styles overriding (by duplicate class names). This functionality is provided by libraries like styled-components, but it's nice to see this natively supported.
In this tutorial, we will be creating two components, a user card and a modal. Using the Rick & Morty API, the web page will load the data and then insert the web component into the DOM. The same will be repeated as the user scrolls down.
Creating the User Cards
The Card will display two details about the character, its image & name, along with a button using which we will open the modal.
To create the web component, we will need to first create a template in markup.
<template>
<style>
/** Styles to be added **/
</style>
<!-- Mark up describing the component will go here -->
</template>
After the template is defined, we will now need to create a class that extends from either HTMLElement
or HTMLUListElement
, HTMLParagraphElement
, etc. If we use the former, the component will be an autonomous custom element, inheriting the minimum properties required. If we use the latter classes, the component will be a customized in-built element, inheriting additional properties.
A web component that inherits from HTMLUListElement
would have left & top margin like most lists have.
<!-- Autonomous custom element -->
<user-card>
</user-card>
<!-- customized in-built element -->
<div is='user-card'>
</div>
It is important to note that the way of using the custom elements will depend on what class is the custom element inheriting from (refer to the code-block above). In this article, we would define the element to inherit from HTMLElement
.
class UserCard extends HTMLElement {
constructor() {
super();
}
}
The above is the minimal amount of code required to declare a custom-element class, in order to make it available to the DOM, we need to define it in the CustomElementRegistry as follows.
window.customElements.define("user-card", UserCard);
That's it, we can now start using <user-card>
, but currently there is nothing defined in the class, we will start by first defining the template (which we discussed earlier). Then define the constructor to do the follows -
- When the custom element is added to the DOM, create a shadow DOM, which would be a child of the custom component.
- Attach the node created from the template into the shadow DOM.
/** Defining the template **/
const template = document.createElement("template");
template.innerHTML = `
<link rel="stylesheet" href="userCard/styles.css">
<div class="user-card">
<img />
<div class="container">
<h3></h3>
<div class="info">
</div>
<button id="open-modal">Show Info</button>
</div>
</div>
`;
The markup defined above will help us create a card that looks like this -
/** Defining the constructor **/
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
In the constructor, we are using attachShadow
to attach a shadow DOM to the current node, then to the shadow DOM, which is accessed using shadowRoot
we will append a child, which is a clone of the template we defined earlier.
So far, the web component should be looking as follows
const template = document.createElement("template");
template.innerHTML = `
<link rel="stylesheet" href="userCard/styles.css">
<div class="user-card">
<img />
<div class="container">
<h3></h3>
<div class="info">
</div>
<button id="open-modal">Show Info</button>
</div>
</div>
`;
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define("user-card", UserCard);
The next step would be to define the life cycle, which should sound familiar if you have some React knowledge. For brevity, we will be focusing only on two methods
- connectedCallback()
- attributeChangedCallback()
ConnectedCallback()
This method is called when the custom element gets mounted on the DOM, this is when we should be defining event listeners, networks calls to fetch data, intervals & timeouts.
To clean up the intervals, timeouts when the custom element is unmounted, we would have to use the disconnectedCallback()
.
attributeChangedCallback()
This method is called when any attribute to the custom element changes (or an attribute is assigned). The method is only called when the attributes defined in the getter observedAttributes()
change their value.
For the user-card component, these methods will be implemented as follows -
static get observedAttributes() {
/** Even though we have other attributes, only defining key here
as to reduce the number of times attributeChangedCallback is called **/
return ["key"];
}
connectedCallback() {
/** Attaching an event-listener to the button so that the
openModal() methods gets invoked in click, openModal will be
defined later **/
this.shadowRoot
.querySelector("#open-modal")
.addEventListener("click", () => this.openModal());
}
attributeChangedCallback(name, oldValue, newValue) {
/** Updating the DOM whenever the key attribute is updated,
helps in avoiding unwanted DOM updates **/
if (name === "key") {
this.shadowRoot.querySelector("h3").innerText = this.getAttribute("name");
this.shadowRoot.querySelector("img").src = this.getAttribute("avatar");
}
}
Creating the Modals
Creating the modal component is similar to creating the user-card component.
The code for the modal -
const modalTemplate = document.createElement('template');
modalTemplate.innerHTML = `
<link rel="stylesheet" href="modal/styles.css">
<div class="modal">
<div class='modal-content'>
<button id='close' class='close'>Close</button>
<img></img>
<h3></h3>
<p></p>
</div>
</div>
`;
class Modal extends HTMLElement {
static get observedAttributes() {
return ['key'];
}
constructor() {
super();
this.showInfo = false;
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(modalTemplate.content.cloneNode(true));
}
connectedCallback() {
this.shadowRoot.querySelector('#close').addEventListener('click', () => {this.remove()});
}
attributeChangedCallback(name, oldValue, newValue) {
if(name==='key'){
this.shadowRoot.querySelector('h3').innerText = this.getAttribute('name');
this.shadowRoot.querySelector('img').src = this.getAttribute('avatar');
this.shadowRoot.querySelector('p').innerHTML = `
Gender: ${this.getAttribute('gender')}
<br/>
Status: ${this.getAttribute('status')}
<br/>
Species: ${this.getAttribute('species')}
`}
}
}
window.customElements.define('user-modal', Modal);
To invoke the modal, we will need to define openModel
in the user-card component. openModal
will create the user-modal
node and assign all the attributes the user-card
had received to the modal, then attach it to the DOM.
openModal() {
const userModal = document.createElement("user-modal");
userModal.setAttribute("name", this.getAttribute("name"));
userModal.setAttribute("avatar", this.getAttribute("avatar"));
userModal.setAttribute("status", this.getAttribute("status"));
userModal.setAttribute("species", this.getAttribute("species"));
userModal.setAttribute("gender", this.getAttribute("gender"));
userModal.setAttribute("key", this.getAttribute("key"));
document
.getElementsByTagName("body")[0]
.insertAdjacentElement("afterend", userModal);
}
Joining all the parts together
The components have been placed in the following folder structure
In the index.html
both the components are imported and a script to fetch the characters data from the Rick and Morty API is defined.
Once the data is fetched, for every character, a user-card
node is created, attributes are assigned, and then inserted into the DOM as follows -
await fetch(`https://rickandmortyapi.com/api/character?page=${page}`)
.then((_) => _.json())
.then((_) => {
_.results.forEach((user, index) => {
max = _.info.pages;
const nodeToBeInserted = document.createElement("user-card");
nodeToBeInserted.setAttribute("name", user.name);
nodeToBeInserted.setAttribute("avatar", user.image);
nodeToBeInserted.setAttribute("status", user.status);
nodeToBeInserted.setAttribute("species", user.species);
nodeToBeInserted.setAttribute("gender", user.gender);
nodeToBeInserted.setAttribute("key", user.id);
document
.getElementById("details")
.insertAdjacentElement("beforeend", nodeToBeInserted);
});
});
page++;
};
An event listener to fetch more data when the user reaches the end of the page.
window.addEventListener(
"scroll",
() => {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 5 && max >= page) {
loadData();
}
},{ passive: true });
This is it! The end result is in the code sandbox below
Conculsion
I hope this article gave you a good glimpse of web components.
If you are interested in knowing more, please check out Web Components at MDN
Edit - As the pointed comment below, creating the Web Component can be made simpler -
Posted on April 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
One thing nearly everyone does is due to incorrect documentation:
can be written as:
Note the use of
append
andappendChild
. In most examplesappendChild
s return value is never used.append
can added multiple text-nodes or elements.And the global template.innerHTML isn't necessary either:
Docs also say you "use super() first in constructor"
That is incorrect also.
You can use any JavaScript before
super()
; you just can not usethis
until it is created by thesuper()
call