paint-brush
Web Components Exposed: Empowering Frontend Developers For Unparalleled Successby@sojinsamuel
746 reads
746 reads

Web Components Exposed: Empowering Frontend Developers For Unparalleled Success

by Sojin SamuelJuly 7th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

With Web Components, you can easily create your own reusable HTML elements. Instead of writing all the HTML, CSS, and JavaScript code from scratch to make it look and function correctly, Web Components simplify the process for you. By doing so, this HTML element will contain all the styles, logic, and functionality, making it reusable.
featured image - Web Components Exposed: Empowering Frontend Developers For Unparalleled Success
Sojin Samuel HackerNoon profile picture

In today's rapidly evolving web development landscape, efficiency and reusability are key factors in building robust web applications. As someone who once struggled to grasp the concept, I can assure you that learning Web Components is a game-changer. In this post, I'll be your guide, taking you through the process step by step. Don't worry, I'll make it super easy for you to understand and implement Web Components.


By the end, you'll be able to create reusable HTML elements like a pro and harness the power of encapsulation and modularity.


So, are you ready to dive in and discover the amazing world of Web Components? Let's get started!

What are Web Components?

With Web Components, you can easily create your own reusable HTML elements. Imagine when you're building a Web App and need a Navbar. Instead of tediously writing all the HTML, CSS, and JavaScript code from scratch to make it look and function correctly, Web Components simplify the process for you.

<style>
    #navbar {
        background-color: white;
        padding: 20px;
        display: flex;
        align-items: space-between;
    }
</style>
<div id="navbar">
    <img src="/images/logo.svg" width="50" height="50" alt="Company name" />
    <h1 id="navbar-page"></h1>
</div>
class MyNavbar {
    constructor() {
        this.navbar = document.querySelector("#navbar");
        this.page = document.querySelector("#navbar-page");
    }
    setPageTitle(title) {
        this.page.textContent = title;
    }
}

Just a friendly note

We organized all the code related to the navbar in one class and file. This is a popular approach for writing front-end code when Web Components are not used. However, with Web Components, we have the ability to create our own custom HTML element, such as app-navbar. By doing so, this HTML element will contain all the styles, logic, and functionality, making it reusable. So, you'll be able to use this web component anywhere in your app just like any other HTML element available in HTML.

<app-navbar></app-navbar>

We created this app-navbar Web Component (don't worry, we'll learn how to do it step by step today!). It's designed to encapsulate all the styles and logic, building a layer on top. When you use this Web Component, you'll always get the same styles and logic. That's why Web Components are so reusable.

Web Components offer several benefits:

  • Reusability: You can reuse a component as many times as you want, making it a versatile tool in web development.
  • Modularity: Web Components are based on modules, which means their definitions are typically contained in a single file. This makes it easier to maintain and locate the logic of a specific web component.
  • Encapsulation: Web Components hide implementation details, allowing you to concentrate on what truly matters for your project.


When you use Components, whether they are Web components or Components created with a front-end library/framework, your app will have this awesome new look!

<app-root>
   <app-navbar page="home"></app-navbar>
   <flashcards-questions>
       <flashcards-question></flashcards-question>
       <flashcards-question></flashcards-question>
       <flashcards-question></flashcards-question>
   </flashcards-questions>
   <app-sidebar></app-sidebar>
</app-root>

Then What about front-end frameworks?

You can use Web Components alongside the components that most front-end frameworks support. You can keep track of their support by visiting custom-elements-everywhere.com.

But Why should I learn web components?

Web Components have been recently added to the Web Platform, and they function seamlessly on all modern browsers. This is incredibly significant because they are integrated directly into the browser, unlike front-end frameworks/libraries. You don't require a separate library to utilize them.


Having experience in constructing Web Components will not only assist you in working with other front-end frameworks but also in comprehending the advanced functionalities offered by the browser.

A Closer Look at the Low Level

I wanted to mention that Web Components are actually a collection of browser APIs that work at a low level. They're really powerful because of their low-level nature, but there's a small drawback: you often end up writing more code than you might anticipate. However, don't worry! There's a fantastic high-level abstraction library called Lit (formerly known as Lit Element) that sits on top of Web Components. It simplifies the development process and makes things much easier.

If you're curious, I can write an article about this topic. Just let me know in the comments section!

Let's talk about the standards involved

When it comes to creating Web Components, there are four main standards that play a crucial role. These standards are:

  • ES Modules
  • Custom Elements
  • Shadow DOM
  • HTML Templates

Hackernoon has done a great job with ES Modules already. Now, let me guide you through the remaining steps in this post, one by one.

Custom Elements

Custom elements are basically your very own custom DOM elements. For instance, you can create elements like:

  • <app-navbar>
  • <flashcards-questions>
  • <flashcards-answer>.

To define your own DOM element, you can utilize the Custom Elements API. Here's the essential code you need to make it work:

class AppNavbar extends HTMLElement {

}

window.customElements.define("app-navbar", AppNavbar);

Once you use the code provided above, you can register the app-navbar as a Custom Element. This will allow you to easily use it in your HTML.

<app-navbar></app-navbar>

It's working! However, you won't see anything on the screen just yet because the custom element is currently empty. But don't worry, we're off to a great start!

Now, let's go through the code together:

We have the class AppNavbar, which defines how your Web Component will behave. This class represents any HTML element on your webpage. Since we're creating a custom DOM element, it needs to inherit from HTMLElement.


If you want me to write about interfaces implemented by the HTML elements, Let me know in the comments


Now, let's talk about the last part: window.customElements.define("component-name-here", NameOfClassThatDescribesIt). This line of code tells the browser that we're creating a new custom element. It takes two arguments: the name of the element and the class that describes it. In our example, we've defined a new custom element called app-navbar (which means you can use it in your HTML as <app-navbar></app-navbar>) and its class definition is AppNavbar.

Choosing a Name for Your Component 🤔

When you're selecting a name for your component, it's important to include a hyphen (-) in the name. This is because single-word names are typically reserved for the browser's use, such as p, div, span, address, table, and so on.


Instead, developers are encouraged to use names that consist of more than one word and are separated by a hyphen (-). This allows you to define custom element names as per your requirements.

This practice ensures that we avoid conflicts in the future. For instance, let's say you named your custom element navbar, but browsers introduce a new HTML element with the same name a few years down the line. This could potentially break your code.

That's why it's essential to name your component using a hyphen. Typically, developers often use the prefix app- to indicate custom elements. Here are a few examples of valid custom element names:

  • app-navbar
  • app-footer
  • app-button
  • my-navbar
  • side-panel
  • chat-widget

Naming the class

Feel free to choose any name you like for the class, but I suggest using an uppercase version of the custom element name. Here are a few examples:

  • If you have an element called <app-navbar>, you can name the class as AppNavbar.
  • For <my-navbar>, you can use the class name MyNavbar.
  • In the case of <side-panel>, the class would be called SidePanel.
  • Lastly, if you have a <chat-widget>, the class name would be ChatWidget.

Remember, these are just recommendations, and you have the flexibility to choose a name that suits your preferences.

How to Override the Constructor?

When you create your own constructor method, make sure to call super() right at the beginning because we're extending from HTMLElement. By doing this, the constructor of HTMLElement will be invoked. If you accidentally forget to call super(), the constructor of HTMLElement won't be executed, and your custom element won't function properly. Instead, it will throw an error.

class AppNavbar extends HTMLElement {
    constructor(){
        super(); // ALWAYS start with super()
    }
}

window.customElements.define("app-navbar", AppNavbar);

Shadow DOM

We've just created our first custom element!

So what's next? Well, now it's time to attach a Shadow DOM to it.

A Shadow DOM is like a special section within the DOM where we can keep our styles and scripts separate from the rest. It acts as a self-contained environment, ensuring that they don't affect or interfere with other elements outside the Shadow DOM.

To better understand, let's consider an example using an element called <dashboard-stats> with its very own Shadow DOM attached to it:

<style>
    p {
        color: red;
    }
</style>
<p>Page subtitle</p>
<dashboard-stats>
    <!-- Shadow DOM starts here -->
    <p>Dashboard stats</p>
    <!-- Shadow DOM ends here -->
</dashboard-stats>

The <p>Page subtitle</p> will be in red, but the <p>Dashboard stats</p> won't! That's because styles don't affect or get affected by the Shadow DOM. This is great news for web development since you can now write CSS specific to a particular component without concerns about its impact on other elements on the page. For instance:

<dashboard-stats>
    <!-- Shadow DOM starts here -->
    <style>
        p {
            font-weight: bold;
        }
    </style>
    <p>Dashboard stats</p>
    <!-- Shadow DOM ends here -->
</dashboard-stats>

The <dashboard-stats> component will make only the paragraphs (<p> elements) inside it appear bold. Any paragraphs outside this component or in other components won't be affected. This feature can greatly simplify the process of creating complex applications.

Attaching the Shadow DOM

To attach the Shadow DOM in the constructor() of your custom element, you can do this:

class DashboardStats extends HTMLElement {
    constructor() {
        super(); // do not forget super() because we're overriding the constructor()
        const shadowRoot = this.attachShadow({mode: "open"});
    }
}

Attaching a Shadow DOM will look like this in DevTools:

Chrome dev tools

We attach the Shadow DOM because we want to have encapsulated styles and scripts. However, we haven't added any HTML yet. We'll include that in this section.

mode: "open" or “closed”

When creating Web Components, it is likely that most, if not all, of them will have an open mode. This allows you to access the Web Component using JavaScript from external sources. We generally do not want to restrict this access. However, if there is a specific reason to prevent it, you can use the closed mode instead. For instance, the browser's built-in <video> element is a Web Component that operates in "closed" mode. But in most cases, the Web Components you create will be in open mode.

Component Lifecycle

Let's talk about the lifecycle of Web Components in a simple way. When you use a Web Component on your page, it goes through different stages that we call the lifecycle.

  1. The constructor() is called when the element is created.
  2. The connectedCallback() is called when the element is inserted into the DOM.
  3. The disconnectedCallback() is called when the element is removed from the DOM.

Keep in mind that these methods will only be called if you define them. For instance, the disconnectedCallback() will only be called when the element is removed from the DOM, assuming you have defined this method in your component.

Usage

  1. constructor(): When you create an element and it's not yet inserted into the DOM, the constructor() function is called. This function is commonly used to attach the shadow root. Remember not to manipulate the DOM during this stage. It's better to do that in the connectedCallback() function, where you can safely access child elements.
  2. connectedCallback(): As soon as the element is inserted into the DOM, the connectedCallback() function is triggered. This is the right place to put any code that relies on the DOM, since the element is now a part of it. Here, you can manage attributes and add event listeners as needed.
  3. disconnectedCallback(): When the element is removed from the DOM, the disconnectedCallback() function is called. This function is often used to clean up and free up memory. Typically, it's used to remove any event listeners that were added in the connectedCallback() function.

Code Example

class AppDashboard extends HTMLElement {
    constructor() {
        super();
        console.log("AppDashboard created");
        const shadowRoot = this.attachShadow({mode: "open"});
    }

    connectedCallback() {
        console.log("AppDashboard inserted into DOM");
        // add event listeners (if necessary)
    }

    disconnectedCallback() {
        console.log("AppDashboard removed from DOM");
        // remove event listeners (that were added in connectedCallback())
    }
}

window.customElements.define("app-dashboard", AppDashboard);

When you're in the constructor(), keep in mind that the element hasn't been inserted into the DOM yet. That's why it's a good idea to put your event listeners in the connectedCallback.

My suggestion is to attach the shadow in the constructor() and handle all other DOM operations in the connectedCallback. This approach will help you visualize the distinction between the two.

Take a peek at the code snippet below for a clearer understanding:

class AppDashboard extends HTMLElement {
    constructor() {
        super();
        console.log("AppDashboard created");
        const shadowRoot = this.attachShadow({mode: "open"});
    }

    connectedCallback() {
        console.log("AppDashboard inserted into DOM");
    }
}

window.customElements.define("app-dashboard", AppDashboard);

const element = document.createElement("app-dashboard"); // "AppDashboard created" will be logged
document.body.appendChild(element); // "AppDashboard inserted into DOM" will be logged

HTML Templates

The <template> element lets you declare HTML and CSS without rendering it right away. You can save it for later use. When you combine it with Web Components, you can define HTML (and CSS) code that can be utilized in a Web Component.

Here's an example:

<template id="dashboard-template">
    <style>
        p {
            font-weight: bold;
        }
    </style>
    <p>Dashboard stats</p>
</template>

This template doesn't display anything, but you'll be able to copy its content later and inject it into the Shadow Root of a Web Component. Remember, we gave this template an id="dashboard-template" so that you can easily find it later using document.querySelector.

Use the Template

class DashboardStats extends HTMLElement {
    constructor() {
        super(); 
        this.attachShadow({mode: "open"});
    }

    connectedCallback() {
        const template = document.querySelector("#dashboard-template");
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
}

window.customElements.define("dashboard-stats", DashboardStats);

When you use this, it injects a copy of the content from the <template> and puts it inside the shadow root of the dashboard-stats. As a result, you'll have an element on your page that displays Dashboard stats in bold.

Take a look at the DevTools to see how it appears.

Chrome Dev tools

Let's go through the code together:

  1. I recommend manipulating the content of the custom element in the connectedCallback, although you can also put this code in the constructor() if you prefer.
  2. To begin, we find the template by using const template = document.querySelector("#dashboard-template").
  3. When you use this.attachShadow, you automatically gain access to the shadow root through this.shadowRoot.
  4. Next, we create a copy of the template's content using cloneNode(true). We need to clone it because the template can only be used once. By cloning it, you'll be able to use it again in the future.
  5. The cloneNode() function has one parameter called deep. When you call cloneNode(true), it sets the deep parameter to true, which ensures that children elements are also cloned.
  6. Finally, we use this.shadowRoot.appendChild() to add the cloned template's HTML into the shadow root of the current custom element.

You might be thinking that setting this up requires a lot of code, and you're absolutely right. That's why many people recommend using a Web Components library like lit.

Let's talk about Attributes and Properties

Your Web Components can also have custom attributes, just like regular HTML elements. For instance:

<app-dashboard theme="dark"></app-dashboard>

To read the theme="dark" attribute from within the component, here's what you can do:

class AppDashboard extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: "open"});
    }

    connectedCallback() {
        console.log(this.getAttribute("theme")); //"dark"
    }
}

window.customElements.define("app-dashboard", AppDashboard);

In HTML, there's a limitation where attributes can only be in the form of strings. Even boolean values need to be enclosed within a string. So, to pass a boolean attribute, like is-dark, you would use is-dark="true" or is-dark="false”, treating it as a string. Afterward, you'll need to convert it to a boolean by comparing it to the string “true”.

const isDark = this.getAttribute("is-dark") === "true" ? true : false;

Please keep in mind that any changes you make to this attribute will not update automatically. There is a process you can follow to subscribe to attribute changes, but that goes beyond the scope of this post. However, if you're interested, I'd be happy to write a dedicated article on this topic. Let me know in the comments if you'd like that 🤗

Properties

Properties are a great way to store information in a Web Component. Unlike attributes, which only accept strings, properties can hold any data type. They are like variables that are attached to the instance of an element. Let me give you an example:

<app-dashboard theme="dark"></app-dashboard>
const dashboard = document.querySelector("app-dashboard");
// existing properties
dashboard.nodeName; // APP-DASHBOARD (automatically created by the browser)

// Define your own property
dashboard.graphs = [{
    //...
}];

The app-dashboard Web Component has a property called dashboard.graphs, which holds an array of objects in this example.

That’s a Wrap

Web Components are just plain HTML, CSS, and JavaScript! They're actually easier to learn compared to some front-end frameworks. But here's the thing: there's a chunk of code that keeps repeating in every component, which can sometimes interfere with your own logic. It becomes even more true when you're dealing with templates that have dynamic data, attributes, and properties.


That's why, if you're interested in working with Web Components, it's recommended to use a small library on top of them. One great example is Lit. It's lightweight, weighing less than 6KB, and it lets you define your Web Component using a user-friendly syntax.


If you'd like a brief introduction to Lit, feel free to let me know in the comments!