Overcome Framework Lock-In

by migrating your UI library to the Web Components API.

Not long time ago, I rediscovered the Web Components API. While many developers are used to popular front-end frameworks, I would reconsider this built-in browser feature in new projects.

Why?

Framework Support

Each time a new framework comes out, we reimplement all the basic UI components like popups, dropdowns, and forms from scratch. In the best case we only have to create a wrapper around available libraries. Even though popular frameworks have many UI libraries to choose from, web components are universal. They might even work faster because of direct DOM manipulations.

Familiarity

Using custom web components is familiar, since it’s the same way we use all the other components. It uses the same approach as in vanilla web development: you write HTML code, style the elements with CSS, and add interactivity with JavaScript. You (almost) don’t have to learn anything if you already know the basics of web development.

Ease of Use

As a developer using a custom HTML element, you only need to import a script defining the element and use the element in your HTML markup. Bundlers aren’t required nor are any external libraries for rendering the component.

When?

I don’t think we should “spam” all our projects with web components. However, whenever we develop a UI component or a library that could be reused in another project, it would make sense to implement it as a custom HTML element. At least if you want to reach the widest audience and/or you aren’t sure whether you’re going to stick with one framework.

If you find it difficult to change back to imperative programming to implement a custom element, consider checking out Lit Element. It’s a library/framework that’s built on top of Web Components API, providing declarative syntax and additional features.

How?

In this article, two examples are provided. If you are willing to learn more, consider the guides from MDN, JavaScript.info, or web.dev.

Counter Element Example

First, create an HTML file index.html with the following layout:

<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Web Components API Demo</title>

        <!-- Connect the script with our custom element to the page -->
        <script src="counter-element.js"></script>
    </head>
    <body>
        <!-- Here is how our custom element looks in the layout! -->
        <counter-element count="1"></counter-element>
    </body>
</html>

Then create a JavaScript file counter-element.js with the following code:

class CounterElement extends HTMLElement {
    // Change of these attributes triggers the ‘attributeChangedCallback’ call
    static observedAttributes = ["count"]

    // This method gets called whenever an observed attribute
    // changed its value (including when it gets set for the first time)
    attributeChangedCallback(attributeName, oldValue, newValue) {
        if (attributeName === "count") {
            this.innerText = `Count: ${newValue}`
        }
    }

    // This method gets called every time
    // the element gets appended to the DOM
    connectedCallback() { /* ... */ }

    // This method gets called every time
    // the element gets unmounted from the DOM
    disconnectedCallback() { /* ... */ }
}

// Before we are able to use the custom element on the page,
// we need to define it in the custom element registry.
// The element name has to have a ‘-’ in it,
// since one-word element names are reserved
customElements.define("counter-element", CounterElement)

If you wish to ensure the value gets updated on attribute change, create an interval that continuously updates the counter attribute.

Customizing built-in elements

In case you want to customize a built-in element, some features and advantages of the Web Components API disappear:

  • HTML syntax changes from <your-component-name> to <built-in-component-name is="your-component-name">

    • In CSS, your-component-name becomes built-in-component-name[is=your-component-name] accordingly
  • You cannot attach shadow DOM to customized built-in components

Still, there are ways to overcome these limitations, which won’t be covered in this article.

Here is an example for a custom button that prints to the console whenever it is being clicked:

class MyCustomButton extends HTMLElement {
    constructor() {
        super()
        this._onClick = () => console.log("Click!")
    }

    connectedCallback() {
        this.addEventListener("click", this._onClick)
    }

    disconnectedCallback() {
        this.removeEventListener("click", this._onClick)
    }
}

customElements.define("my-custom-button", MyCustomButton, {
    extends: "button"
})

// And now you can use it in HTML the following way:
// <button is="my-custom-button">Click me</button>

Summary

This article has covered reasons to use Web Components API more often, listed some situations when one should do that, and offered code samples and resources to deepen one’s knowledge.

Published on October 12, 2024

Web Development