Web Components!

⏳ 8 min

Componentizing straight from the platform itself.

Let’s dive into the world of Web Components, a powerful and fascinating technology that, despite its potential, often takes a backseat to popular modern frameworks like React, Angular, or Vue. Currently, web development is dominated by these frameworks, each with its own characteristics and philosophies. However, Web Components offer a native way to create reusable components that are framework-agnostic, allowing their use in any web application without the need to rely on external libraries.

This technology consists of three fundamental pillars: Custom Elements, Shadow DOM, and HTML Templates. Each of these elements plays a crucial role in creating components that are as powerful as they are easy to use.

Let’s take a closer look at each one:

1. Custom Elements

Custom Elements allow you to define your own HTML tags. So, instead of using a simple <div>, you can have something like <my-component>.

In our case, we’ll illustrate these concepts by creating a component that represents a “card”. This practical example will help us better understand how Web Components work and how we can leverage them to build reusable and modular user interfaces.

class CardComponent extends HTMLElement {
  constructor() {
    super();
  }
}

customElements.define('card-component', CardComponent);

With this, when you use <card-component></card-component> in your HTML, the browser will know what to do with it.

2. Shadow DOM

Shadow DOM allows you to encapsulate the HTML, CSS, and JavaScript of the component so that it is not affected by, and does not affect, the rest of the application.

The creation of the Shadow DOM is done within the constructor of the web component.

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

customElements.define('card-component', CardComponent);

Through the attachShadow method, we create the Shadow DOM and attach it to the component instance.

If we add the open mode parameter, the Shadow DOM is accessible from the outside through the element’s shadowRoot property. This means you can access and manipulate the Shadow DOM content using JavaScript. However, if it is closed, the Shadow DOM is not accessible from the outside. You cannot access the shadowRoot property from outside the component.

3. HTML Templates

These are HTML code blocks that you define once and can reuse whenever you want.

There are two main ways to define our content: using HTML Templates or using Template Literals. Let’s explore both approaches.

HTML Templates are defined in your HTML but are not immediately rendered. They are activated only when needed. Here’s a simple example:

<template id="card-template">
  <style>
    .card {
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 16px;
      max-width: 300px;
      box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
    }
  </style>
  <div class="card">
    <img>
    <h1></h1>
    <p></p>
    <a></a>
  </div>
</template>

Template Literals are another way to define your content, but you do it directly in your JavaScript. They are very useful when you need something more dynamic or when you want to keep all your code in one place. Here’s an example:

const templateLiteral = `
  <style>
    .card {
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 16px;
      max-width: 300px;
      box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
    }
  </style>
  <div class="card">
    <img src="" alt="Image">
    <h1>Título</h1>
    <p>Descripción</p>
    <a href="#">Enlace</a>
  </div>
`;

shadowRoot.innerHTML = templateLiteral;

What’s the Difference?

HTML Templates:

Template Literals:

Full Example of Creating a Web Component

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Component Card</title>
</head>
<body>
  <template id="card-template">
    <style>
      .card {
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 16px;
        max-width: 300px;
        box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
      }
      .card img {
        max-width: 100%;
        border-radius: 8px;
      }
      .card h1 {
        font-size: 1.5em;
        margin: 0.5em 0;
      }
      .card p {
        font-size: 1em;
        color: #666;
      }
      .card a {
        display: inline-block;
        margin-top: 1em;
        padding: 0.5em 1em;
        background-color: #007bff;
        color: white;
        text-decoration: none;
        border-radius: 4px;
      }
      .card a:hover {
        background-color: #0056b3;
      }
    </style>
    <div class="card">
      <img src="https://via.placeholder.com/300">
      <h1>Title</h1>
      <p>Description</p>
      <a>Link</a>
    </div>
  </template>

  <script>
    class CardComponent extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        
        const template = document.getElementById('card-template').content;
        this.shadowRoot.appendChild(template.cloneNode(true));
      }
    }
    customElements.define('card-component', CardComponent);
  </script>

  <card-component></card-component>
</body>
</html>

Result:

web component example

At this point, we have covered the basics and learned how to create static components that encapsulate their structure and styles, providing us with a powerful way to build reusable and modular elements directly from the platform.

Furthermore, you can now see how Web Components can enhance your projects. From defining your own HTML tags to encapsulating styles and behaviors, this technology offers you a flexibility and control that modern frameworks often hide behind layers of abstraction.

Would you like to discover how to make your components dynamic and responsive to changes in their environment? Do you want to know exactly when they are created, updated, and destroyed, and how you can leverage these moments to write more efficient and robust code? This is where the lifecycle methods of Web Components come into play.

Lifecycle of Web Components

Web Components have a specific lifecycle that includes several callbacks you can use to execute code at different stages of the component’s life. The four main callbacks are:

Let’s look at a complete example, where we will add dynamism to our <card-component> using lifecycle methods and also add our own methods to the component to separate its logic.

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

  static get observedAttributes() {
    return ['title', 'image', 'description', 'link-text', 'link-href', 'event-name'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.render();
  }

  connectedCallback() {
    this.render();
    this.addEventListeners();
  }

  disconnectedCallback() {
    this.removeEventListeners();
  }

  styles() {
    return `
      <style>
        :host {
          display: block;
          max-width: 300px;
          margin: 10px;
        }
        .card {
          border: 1px solid #ccc;
          border-radius: 8px;
          padding: 16px;
          box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
        }
        .card img {
          max-width: 100%;
          border-radius: 8px;
        }
        .card h1 {
          font-size: 1.5em;
          margin: 0.5em 0;
        }
        .card p {
          font-size: 1em;
          color: #666;
        }
        .card a {
          display: inline-block;
          margin-top: 1em;
          padding: 0.5em 1em;
          background-color: #007bff;
          color: white;
          text-decoration: none;
          border-radius: 4px;
        }
        .card a:hover {
          background-color: #0056b3;
        }
      </style>
    `;
  }

  render() {
    this.shadowRoot.innerHTML = `
      ${this.styles()}
      <div class="card">
        <img src="${this.getAttribute('image')}" alt="${this.getAttribute('title') || 'Image'}">
        <h1>${this.getAttribute('title')}</h1>
        <p>${this.getAttribute('description')}</p>
        <a href="${this.getAttribute('link-href')}">${this.getAttribute('link-text') || 'Enlace'}</a>
      </div>
    `;
  }

  addEventListeners() {
    const link = this.shadowRoot.querySelector('a');
    const eventName = this.getAttribute('event-name') || 'card-link-click';

    link.addEventListener('click', this.linkClickHandler = (e) => {
      e.preventDefault();
      this.dispatchEvent(new CustomEvent(eventName, {
        detail: {
          message: 'Link clicked!',
          href: link.getAttribute('href')
        },
        bubbles: true,
        composed: true
      }));
    });
  }

  removeEventListeners() {
    const link = this.shadowRoot.querySelector('a');
    link.removeEventListener('click', this.linkClickHandler);
  }
}

customElements.define('card-component', CardComponent);
<card-component
  title="My Card Title"
  image="https://via.placeholder.com/300"
  description="This is the card title description"
  link-text="More info..."
  link-href="https://example.com"
  event-name="custom-link-click">
</card-component>

<script src="index.js"></script>
<script>
  document.querySelector('card-component').addEventListener('custom-link-click', (e) => {
    console.log(e.detail.message);
    console.log(e.detail.href);
  });
</script>

Summary of our component:

  1. constructor: Called when an instance of the component is created or updated. Here, we initialize the Shadow DOM.
  2. static get observedAttributes(): Specifies which attributes to observe. When any of these attributes change, attributeChangedCallback is called.
  3. attributeChangedCallback(name, oldValue, newValue): Called when one of the observed attributes is changed. Here, we simply re-render the component.
  4. connectedCallback(): Called when the component is added to the DOM. Here, we render the content and add event listeners.
  5. disconnectedCallback(): Called when the component is removed from the DOM. Here, we remove the event listeners.
Other methods we have added, but which are not part of the component’s lifecycle:
  1. styles(): Returns the CSS styles for the component.
  2. render(): Generates and adds the component’s HTML content to the Shadow DOM.
  3. addEventListeners(): Adds event listeners to the component, such as a click on the link.
  4. removeEventListeners: Removes the event listeners from the component when it is removed from the DOM.

Farewell 👋

And so we come to the end of our exciting journey through the world of Web Components. We have learned how to create custom components, encapsulate styles, manage the lifecycle, and make our components dynamic. From creating a simple card with a title, image, description, and link, to understanding how and when these components are created, updated, and destroyed.

Web Components may seem like a technology hidden under the shadow of major frameworks like React or Angular, but they offer a flexibility and control that is worth exploring. So, if you ever feel curious or need a lightweight and modular solution for your projects, don’t hesitate to give Web Components a try.

Happy coding 💻

👈 Go back