¡Web components!

⏳ 8 min

Como componentizar desde la propia plataforma.

Vamos a sumergirnos en el mundo de los Web Components, una tecnología potente y fascinante que, a pesar de ello, a menudo queda en segundo plano frente a los populares frameworks modernos como React, Angular, o Vue. En la actualidad, el desarrollo web se encuentra dominado por estos frameworks, cada uno con sus propias características y filosofías. Sin embargo, los Web Components ofrecen una forma nativa de crear componentes reutilizables que son agnósticos al framework, permitiendo su uso en cualquier aplicación web sin necesidad de depender de librerías externas.

Esta tecnología se compone de tres pilares fundamentales: Custom Elements, Shadow DOM, y HTML Templates. Cada uno de estos elementos juega un papel crucial en la creación de componentes que son tan potentes como fáciles de usar.

Vamos a ver cada una con más detalle:

1. Custom elements

Los Custom Elements te permiten definir tus propias etiquetas HTML. Así que, en lugar de usar un simple <div>, puedes tener algo como <mi-componente>.

En nuestro caso, vamos a ilustrar los conceptos creando un componente que represente una “card”. Este ejemplo práctico nos permitirá entender mejor cómo funcionan los Web Components y cómo podemos aprovecharlos para construir interfaces de usuario reutilizables y modulares.

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

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

Con esto, cuando uses <card-component></card-component> en tu HTML, el navegador sabrá qué hacer con él.

2. Shadow DOM

El Shadow DOM te permite encapsular el HTML, CSS y JavaScript del componente para que no afecten ni sean afectados por el resto de la aplicación.

La creación del Shadow DOM se lleva a cabo desde el constructor del web component

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

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

Mediante el método attachShadow creamos el Shadow DOM y lo adjuntamos a la instancia del componente. Si añadimos el parámetro mode open, el Shadow DOM es accesible desde el exterior a través de la propiedad shadowRoot del elemento. Esto significa que puedes acceder y manipular el contenido del Shadow DOM usando JavaScript. Pero si es closed, el Shadow DOM no es accesible desde el exterior. No puedes acceder a la propiedad shadowRoot desde fuera del componente.

3. HTML Templates

Son los bloques de código HTML que defines una vez y luego podrás reutilizar siempre que quieras.

Tenemos dos formas principales para definir nuestro contenido: usando HTML Templates o usando Template Literals. Vamos a ver ambas formas.

Los HTML Templates los defines en tu HTML, pero no se renderizan de inmediato. Solo se activan cuando los necesitas. Aquí tienes un ejemplo simple:

<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>

Los Template Literals son otra forma de definir tu contenido, pero lo haces directamente en tu JavaScript. Son súper útiles cuando necesitas algo más dinámico o cuando quieres tener todo tu código en un solo lugar. Aquí tienes un ejemplo:

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;

¿Cuál es la diferencia?

HTML Templates:

Template Literals:

Ejemplo completo de creación de un 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>

Resultado:

web component example

En este punto hemos cubierto los conceptos básicos y aprendido a crear componentes estáticos que encapsulan su estructura y estilos, brindándonos una manera poderosa de construir elementos reutilizables y modulares desde la propia plataforma.

Además, ya puedes ver cómo los Web Components pueden mejorar tus proyectos. Desde definir tus propias etiquetas HTML hasta encapsular estilos y comportamientos, esta tecnología te ofrece una flexibilidad y control que los frameworks modernos a menudo ocultan tras capas de abstracción.

¿Te gustaría descubrir cómo hacer que tus componentes sean dinámicos y respondan a cambios en su entorno? ¿Quieres saber exactamente cuándo se crean, actualizan y destruyen, y cómo puedes aprovechar estos momentos para escribir código más eficiente y robusto? Aqui es donde entran en escena los métodos del ciclo de vida de los Web Components

Ciclo de vida de los Web Components

Los Web Components tienen un ciclo de vida específico que incluye varios callbacks que puedes usar para ejecutar código en diferentes etapas de la vida del componente. Los cuatro callbacks principales son:

Vamos a ver un ejemplo completo, donde le vamos a añadir dinamismo a nuestro <card-component> utilizando los métodos del ciclo de vida y también añadiremos nuestros própios métodos al componente para separar su lógica.

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>

Resumen de nuestro componente:

  1. contructor: Se llama cuando una instancia del componente es creada o actualizada. Aquí, inicializamos el Shadow DOM.
  2. static get observedAttributes(): Especifica qué atributos observar. Cuando cualquiera de estos atributos cambia, se llama a attributeChangedCallback.
  3. attributeChangedCallback(name, oldValue, newValue): Se llama cuando se cambia uno de los atributos observados. Aquí, simplemente volvemos a renderizar el componente.
  4. connectedCallback(): Se llama cuando el componente se añade al DOM. Aquí, renderizamos el contenido y añadimos los event listeners.
  5. disconnectedCallback(): Se llama cuando el componente se elimina del DOM. Aquí, eliminamos los event listeners.
Otros métodos que hemos añadido, pero que no forman parte del ciclo de vida del componente:
  1. styles(): Devuelve los estilos CSS para el componente.
  2. render(): Genera y añade el contenido HTML del componente al Shadow DOM.
  3. addEventListeners(): Añade event listeners al componente, como el clic en el enlace.
  4. removeEventListeners: Elimina los event listeners del componente cuando el componente se elimina del DOM.

Despedida 👋

Y así llegamos al final de nuestro emocionante viaje por el mundo de los Web Components. Hemos aprendido cómo crear componentes personalizados, encapsular estilos, manejar el ciclo de vida y hacer que nuestros componentes sean dinámicos. Desde crear una simple tarjeta con título, imagen, descripción y enlace, hasta entender cómo y cuándo se crean, actualizan y destruyen estos componentes.

Los Web Components pueden parecer una tecnología oculta bajo la sombra de los grandes frameworks como React o Angular, pero ofrecen una flexibilidad y control que vale la pena explorar. Así que, si alguna vez sientes curiosidad o necesitas una solución ligera y modular para tus proyectos, no dudes en darle una oportunidad a los Web Components.

Happy coding 💻

👈 Volver