Web Components Part I

Web Components Part I

Published: 7/28/20244 min read
JS

Like most frontend developers, I rely heavily on a frontend framework for creating and consuming reusable components. However, I came across some interesting cases where a "universal" or "native" approach for writing components comes in handy.

In the first part of this series we'll go over declaring and defining web components via the custom elements spec. We'll see how we can start implementing some basic markup, define props and how to use lifecycle callbacks (or hooks) to address prop changes (reactivity 101). By the end of this series we'll see how we can leverage Vue's custom element mode for creating web components with ease 😎.

Why?

IMO, there are three main reasons for using web components:

  • No frontend framework needed
  • Excellent browser support
  • Create reusable, reactive components just like you would create them with your frontend framework of choice

What are web components?

The term "web components" refers to a combination of a few web specs that allow us to create framework-agnostic reusable components. MDN and webcomponents.org list three essential specs for web components:

  1. Custom Elements
  2. Shadow DOM
  3. HTML Template

webcomponents.org, which is an amazing resource for web components, also lists the ES Modules spec as an essential part of web components. In this series, we won't explore all the specs in depth. However, we will go over some examples that cover quite a bit of common use cases like using props, handling state, lifecycle hooks and styling. Now, let's create our first web component!

Defining a custom element

The most basic implementation of a custom element consists of an HTMLElement sub class and defining (or registering) it as a custom element. In the following example, we can see that after calling the super method we can start implementing all our component's properties, markup and functionality.

// Custom element declaration
class MyCustomElement extends HTMLElement {
  constructor () {
    super()
    this.innerHTML = `<p>${this.textContent}</p>`
  }
}
// Register/Define the custom element
window.customElements.define('my-custom-element', MyCustomElement);

And we'll use it in our HTML like this:

<my-custom-element>A new custom element</my-custom-element>

Let's take this example and modify it so that our component has a user-name property (or "prop" 😅). You can define a custom prop with an HTML attribute:

// Custom element declaration
class MyCustomElement extends HTMLElement {
  constructor () {
    super();
    this.innerHTML = `<p>${this.title}</p>`
  }
  
  get title () {
    const userName = this.getAttribute('user-name') || '';
    return `My name is ${userName}`
  }
}
// Register/Define the custom element
window.customElements.define('my-custom-element', MyCustomElement);
<my-custom-element user-name="Lorem"></my-custom-element>
<my-custom-element user-name="Ipsum"></my-custom-element>

Now let's handle a case where the user-name prop changes. We need to set the static observedAttributes property and the attributeChangedCallback method ("lifecycle method"). Then, we'll need to re-render the element.

// Custom element declaration
class MyCustomElement extends HTMLElement {
  static observedAttributes = ["user-name"];

  constructor () {
    super();
    this.render()
  }
  
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'user-name') {
      this.render()
    }
  }
  
  render () {
    this.innerHTML = `<p>${this.title}</p>`
  }
  
  get title () {
    const userName = this.getAttribute('user-name') || '';
    return `My name is ${userName}`
  }
}
// Register/Define the custom element
window.customElements.define('my-custom-element', MyCustomElement);

setTimeout(() => {
  const firstExampleEl = document.getElementById('example-1')
  if (firstExampleEl) {
    document.getElementById('example-1').setAttribute('user-name', 'Yossi')
  }
}, 3000)
<my-custom-element id="example-1" user-name="Lorem"></my-custom-element>
<my-custom-element user-name="Ipsum"></my-custom-element>

We went over very basic examples for using "props" and a "lifecycle" method with our custom element and we've only used a subset of the custom elements spec. For an excellent overview of all lifecycle methods see the MDN documentation for custom element lifecycle callbacks.

In the next part of this tutorial, we'll look at several ways of styling our custom elements. We'll see how we can use globally available styles as well as fully encapsulating our styles in a custom element with the Shadow DOM spec 👻.