Todos tenemos proyectos en los que preferiríamos no trabajar. El código se ha vuelto inmanejable, el alcance del proyecto ha evolucionado, se han aplicado correcciones rápidas sobre otras correcciones y la estructura se ha derrumbado bajo su peso de código espagueti. Codificar puede ser un asunto desordenado.

Los proyectos se benefician del uso de módulos simples e independientes que tienen una única responsabilidad. El código modular está encapsulado, por lo que hay menos necesidad de preocuparse por la implementación. Mientras sepas lo que producirá un módulo cuando se le dé un conjunto de entradas, no tendrás que entender necesariamente cómo has conseguido ese objetivo.

Aplicar los conceptos modulares a un único lenguaje de programación es sencillo, pero el desarrollo web requiere una mezcla variada de tecnologías. Los navegadores analizan HTML, CSS y JavaScript para representar el contenido, los estilos y la funcionalidad de la página.

No siempre se mezclan fácilmente porque:

  • El código relacionado puede dividirse entre tres o más archivos, y
  • Los estilos globales y los objetos JavaScript pueden interferir entre sí de forma inesperada.

Estos problemas se suman a los que tienen los tiempos de ejecución del lenguaje, los frameworks, las bases de datos y otras dependencias utilizadas en el servidor.

Consulta Nuestro Videotutorial Sobre Componentes Web

¿Qué son los componentes web?

Un Componente Web es una forma de crear un bloque de código encapsulado y de responsabilidad única que puede reutilizarse en cualquier página.

Considera la etiqueta HTML <video>. Dada una URL, un espectador puede utilizar controles como reproducir, pausar, retroceder, avanzar y ajustar el volumen.

Se proporciona el estilo y la funcionalidad, aunque se pueden hacer modificaciones utilizando varios atributos y llamadas a la API de JavaScript. Se puede colocar cualquier número de elementos <video> dentro de otras etiquetas, y no entrarán en conflicto.

¿Y si necesitas tu propia funcionalidad personalizada? Por ejemplo, un elemento que muestre el número de palabras en la página? No hay etiqueta HTML <wordcount> (todavía).

Frameworks como React y Vue.js permiten a los desarrolladores crear componentes web en los que el contenido, el estilo y la funcionalidad se pueden definir en un único archivo JavaScript. Estos resuelven muchos problemas de programación complejos, pero ten en cuenta que:

  • Debes aprender a utilizar ese marco y actualizar tu código a medida que evoluciona.
  • Un componente escrito para un marco de trabajo rara vez es compatible con otro.
  • La popularidad de los frameworks aumenta y disminuye. Se volverá dependiente de los caprichos y prioridades del equipo de desarrollo y de los usuarios.
  • Los Componentes Web estándar pueden añadir funcionalidades al navegador, que son difíciles de conseguir solo con JavaScript (como el Shadow DOM).

Afortunadamente, los conceptos populares introducidos en librerías y frameworks suelen llegar a los estándares web. Ha tardado algún tiempo, pero los Web Components han llegado.

Breve historia de los componentes web

Después de muchos comienzos falsos de proveedores específicos, el concepto de Componentes Web estándar fue introducido por primera vez por Alex Russell en la Conferencia Fronteers en 2011. La biblioteca Polymer de Google (un polyfill basado en las propuestas actuales) llegó dos años después, pero las primeras implementaciones no aparecieron en Chrome y Safari hasta 2016.

Los proveedores de navegadores se tomaron su tiempo para negociar los detalles, pero los Componentes Web se añadieron a Firefox en 2018 y a Edge en 2020 (cuando Microsoft cambió al motor Chromium).

Es comprensible que pocos desarrolladores hayan querido o podido adoptar los Web Components, pero por fin hemos alcanzado un buen nivel de soporte de los navegadores con APIs estables. No todo es perfecto, pero son una alternativa cada vez más viable a los componentes basados en frameworks.

Incluso si no estás dispuesto a deshacerte de tu favorito todavía, los Web Components son compatibles con todos los frameworks, y las APIs recibirán soporte durante años.

Los repositorios de Componentes Web pre-construidos están disponibles para que todos puedan echar un vistazo:

… ¡pero escribir tu propio código es más divertido!

Este tutorial proporciona una completa introducción a los Componentes Web escritos sin un framework de JavaScript. Aprenderás qué son y cómo adaptarlos a tus proyectos web. Necesitarás algunos conocimientos de HTML5, CSS y JavaScript.

Introducción a los componentes web

Los Web Components son elementos HTML personalizados como <hello-world></hello-world>. El nombre debe contener un guión para no chocar nunca con los elementos admitidos oficialmente en la especificación HTML.

Debes definir una clase ES2015 para controlar el elemento. Puede tener cualquier nombre, pero HelloWorld es una práctica común. Debe extender la interfaz HTMLElement, que representa las propiedades y métodos por defecto de todo elemento HTML.

Nota: Firefox permite extender elementos HTML específicos como HTMLParagraphElement, HTMLImageElement o HTMLButtonElement. Esto no está soportado en otros navegadores y no permite crear un Shadow DOM.

Para hacer algo útil, la clase requiere un método llamado connectedCallback() que se invoca cuando el elemento se añade a un documento:

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!';
  }

}

En este ejemplo, el texto del elemento se establece como «Hola Mundo».

La clase debe ser registrada en el CustomElementRegistry para definirla como manejadora de un elemento específico:

customElements.define( 'hello-world', HelloWorld );

El navegador asocia ahora el elemento <hello-world> con tu clase HelloWorld cuando se carga tu JavaScript (por ejemplo, <script type="module" src="./helloworld.js"></script>).

Ya tienes un elemento personalizado.

Demostración de CodePen

Este componente puede ser estilizado en CSS como cualquier otro elemento:

hello-world {
  font-weight: bold;
  color: red;
}

Añadir atributos

Este componente no es beneficioso, ya que el mismo texto se emite independientemente. Como cualquier otro elemento, podemos añadir atributos HTML:

<hello-world name="Craig"></hello-world>

Esto podría anular el texto para que se muestre «¡Hola Craig!». Para lograr esto, puedes añadir una función constructora() a la clase HelloWorld, que se ejecuta cuando se crea cada objeto. Debe:

  1. llamar al método super() para inicializar el HTMLElement padre, y
  2. hacer otras inicializaciones. En este caso, definiremos una propiedad name que se establece por defecto en «World»:
class HelloWorld extends HTMLElement {

  constructor() {
    super();
    this.name = 'World';
  }

  // more code...

Tu componente solo se preocupa por el atributo name. Una propiedad estática observedAttributes() debe devolver un array de propiedades a observar:

// component attributes
static get observedAttributes() {
  return ['name'];
}

Un método attributeChangedCallback() es llamado cuando un atributo es definido en el HTML o cambiado usando JavaScript. Se le pasa el nombre de la propiedad, el valor antiguo y el nuevo valor:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;

}

En este ejemplo, solo se actualizaría la propiedad nombre, pero podría añadir otras propiedades si fuera necesario.

Por último, hay que modificar el mensaje en el método connectedCallback():

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!`;

}

Demostración de CodePen

Métodos del ciclo de vida

El navegador llama automáticamente a seis métodos a lo largo del ciclo de vida del estado del Componente Web. La lista completa se proporciona aquí, aunque ya has visto los cuatro primeros en los ejemplos anteriores:

constructor()

Se llama cuando el componente se inicializa por primera vez. Debe llamar a super() y puede establecer cualquier valor por defecto o realizar otros procesos de pre-renderización.

static observedAttributes()

Devuelve un array de atributos que el navegador observará.

attributeChangedCallback(propertyName, oldValue, newValue)

Se llama cada vez que se modifica un atributo observado. Los definidos en HTML se pasan inmediatamente, pero JavaScript puede modificarlos:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

Es posible que el método tenga que volver a renderizar cuando esto ocurra.

connectedCallback()

Esta función es llamada cuando el Componente Web es anexado a un Modelo de Objeto de Documento. Debe ejecutar cualquier renderización requerida.

disconnectedCallback()

Se llama cuando el Componente Web es eliminado de un Modelo de Objeto de Documento. Esto puede ser útil si necesitas hacer una limpieza, como eliminar el estado almacenado o abortar las peticiones Ajax.

adoptedCallback()

Esta función se llama cuando un componente web se mueve de un documento a otro. Es posible que encuentres un uso para esto, aunque me ha costado pensar en algún caso.

Cómo interactúan los componentes web con otros elementos

Los Componentes Web ofrecen una funcionalidad única que no encontrarás en los frameworks de JavaScript.

El Shadow DOM

Aunque el componente web que hemos construido arriba funciona, no es inmune a las interferencias externas, y CSS o JavaScript podrían modificarlo. Del mismo modo, los estilos que definas para tu componente podrían filtrarse y afectar a otros.

El Shadow DOM resuelve este problema de encapsulación adjuntando un DOM separado al Web Component con:

const shadow = this.attachShadow({ mode: 'closed' });

El modo puede ser:

  1. «open» – JavaScript en la página exterior puede acceder al DOM de la sombra (usando Element.shadowRoot), o
  2. «closed» – solo se puede acceder al Shadow DOM dentro del Web Component.

El Shadow DOM puede ser manipulado como cualquier otro elemento del DOM:

connectedCallback() {

  const shadow = this.attachShadow({ mode: 'closed' });

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

El componente ahora renderiza el texto «Hola» dentro de un elemento <p> y lo estiliza. No puede ser modificado por JavaScript o CSS fuera del componente, aunque algunos estilos como la fuente y el color se heredan de la página porque no fueron definidos explícitamente.

Demostración de CodePen

Los estilos asignados a este componente web no pueden afectar a otros párrafos de la página ni a otros componentes de <hello-world>.

Ten en cuenta que el selector CSS :host puede estilizar el elemento exterior <hello-world> desde el componente web:

:host {
  transform: rotate(180deg);
}

También puedes establecer los estilos que se aplicarán cuando el elemento utilice una clase específica, por ejemplo, <hello-world class="rotate90">:

:host(.rotate90) {
  transform: rotate(90deg);
}

Plantillas HTML

Definir el HTML dentro de un script puede resultar poco práctico para componentes web más complejos. Una plantilla te permite definir un trozo de HTML en tu página que tu Componente Web puede utilizar. Esto tiene varias ventajas:

  1. Puedes modificar el código HTML sin tener que reescribir las cadenas dentro de su JavaScript.
  2. Los componentes pueden ser personalizados sin tener que crear clases JavaScript separadas para cada tipo.
  3. Es más fácil definir el HTML en HTML – y puede ser modificado en el servidor o en el cliente antes de que el componente se renderice.

Las plantillas se definen en una etiqueta <template>, y es práctico asignar un ID para poder referenciarlo dentro de la clase del componente. Este ejemplo tres párrafos para mostrar el mensaje «Hola»:

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p>

</template>

La clase Web Component puede acceder a esta plantilla, obtener su contenido y clonar los elementos para asegurarse de que está creando un fragmento de DOM único en todos los lugares donde se utiliza:

const template = document.getElementById('hello-world').content.cloneNode(true);

El DOM puede modificarse y añadirse directamente al Shadow DOM:

connectedCallback() {

  const

    shadow = this.attachShadow({ mode: 'closed' }),
    template = document.getElementById('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ this.name }`;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg );

  shadow.append( template );

}

Demostración de CodePen

Ranuras para plantillas

Las ranuras te permiten personalizar una plantilla. Supongamos que quieres utilizar tu componente web <hello-world> pero colocar el mensaje dentro de un encabezado <h1> en el DOM de la sombra. Podrías escribir este código:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>

</hello-world>

(Observa el atributo de slot).

Opcionalmente podrías querer añadir otros elementos como otro párrafo:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Las ranuras ahora pueden ser implementadas dentro de su plantilla:

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>

Un atributo de ranura de elemento establecido como «msgtext» (el <h1> ) se inserta en el punto donde hay un <slot> llamado «msgtext». El <p> no tiene un nombre de ranura asignado, pero se utiliza en el siguiente <slot> sin nombre disponible. En efecto, la plantilla se convierte en:

<template id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>This text will become part of the component.</p>
  </slot>

</template>

En realidad no es tan sencillo. Un elemento <slot> en el Shadow DOM apunta a los elementos insertados. Solo se puede acceder a ellos localizando un <slot> y luego utilizando el método .assignedNodes() para devolver un array de hijos internos. El método connectedCallback() actualizado:

connectedCallback() {

  const
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name }`;

  // append shadow DOM
  shadow.append(
    document.getElementById('hello-world').content.cloneNode(true)
  );

  // find all slots with a hw-text class
  Array.from( shadow.querySelectorAll('slot.hw-text') )

    // update first assignedNode in slot
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg );

}

Demostración de CodePen

Además, no se puede aplicar estilo directamente a los elementos insertados, aunque sí se puede apuntar a ranuras específicas dentro de su componente web:

<template id="hello-world">

  <style>
    slot[name="msgtext"] { color: green; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Las ranuras de las plantillas son un poco inusuales, pero una de las ventajas es que su contenido se mostrará si el JavaScript no se ejecuta. Este código muestra un encabezado y un párrafo por defecto que sólo se reemplazan cuando la clase Web Component se ejecuta con éxito:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Por lo tanto, podrías implementar alguna forma de mejora progresiva, aunque solo sea un mensaje de «Necesitas JavaScript».

El Shadow DOM declarativo

Los ejemplos anteriores construyen un Shadow DOM utilizando JavaScript. Esa sigue siendo la única opción, pero se está desarrollando un Shadow DOM declarativo experimental para Chrome. Esto permite el renderizado del lado del servidor y evita cualquier cambio de diseño o destellos de contenido sin estilo.

El siguiente código es detectado por el parser HTML, que crea un Shadow DOM idéntico al que creaste en la última sección (tendrías que actualizar el mensaje según sea necesario):

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

La función no está disponible en ningún navegador, y no hay garantía de que llegue a Firefox o Safari. Puedes encontrar más información sobre el Shadow DOM declarativo, y un polyfill es sencillo, pero ten en cuenta que la implementación podría cambiar.

Eventos de Shadow DOM

Tu Componente Web puede adjuntar eventos a cualquier elemento en el Shadow DOM como lo harías en el DOM de la página, como por ejemplo escuchar los eventos de clic en todos los hijos internos:

shadow.addEventListener('click', e => {

  // do something

});

A menos que se detenga la propagación, el evento se expandirá en el DOM de la página, pero el evento será reorientado. Por lo tanto, parece venir de su elemento personalizado en lugar de elementos dentro de él.

Uso de componentes web en otros frameworks

Cualquier Componente Web que crees funcionará en todos los frameworks de JavaScript. Ninguno de ellos conoce o se preocupa por los elementos HTML – tu componente <hello-world> será tratado de forma idéntica a un <div> y colocado en el DOM donde se activará la clase.

custom-elements-everywhere.com proporciona una lista de frameworks y notas de Web Component. La mayoría son totalmente compatibles, aunque React.js tiene algunos problemas. Es posible utilizar <hello-world> en JSX:

import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  );

}

ReactDOM.render(<MyPage />, document.getElementById('root'));

…pero:

  • React solo puede pasar tipos de datos primitivos a los atributos HTML (no arrays u objetos)
  • React no puede escuchar los eventos de Web Component, por lo que debes adjuntar manualmente tus propios manejadores.

Críticas y problemas de los componentes web

Los componentes web han mejorado mucho, pero algunos aspectos pueden ser difíciles de gestionar.

Dificultades de estilización

El estilo de los Components Web plantea algunos retos, sobre todo si se quiere anular los estilos de alcance. Hay muchas soluciones:

  1. Evita utilizar el Shadow DOM. Podrías añadir contenido directamente a tu elemento personalizado, aunque cualquier otro JavaScript podría cambiarlo accidental o maliciosamente.
  2. Utiliza las clases :host. Como vimos anteriormente, el CSS de ámbito puedes aplicar estilos específicos cuando se aplica una clase al elemento personalizado.
  3. Comprueba las propiedades personalizadas (variables) de CSS. Las propiedades personalizadas se trasladan en cascada a los Web Components, por lo que, si tu elemento utiliza var(--my-color), puedes establecer --my-color en un contenedor externo (como :root), y se utilizará.
  4. Aprovecha las partes de sombra. El nuevo selector ::part() puede aplicar estilo a un componente interior que tenga un atributo part, es decir, <h1 part="heading"> dentro de un componente <hello-world> puede aplicarse estilo con el selector hello-world::part(heading).
  5. Pasa una cadena de estilos. Puedes pasarlos como un atributo para aplicarlos dentro de un bloque < style>.

Ninguno es ideal, y tendrás que planificar cuidadosamente la forma en que otros usuarios pueden personalizar sus componente web.

Entradas ignoradas

Cualquier campo <input>, <textarea>, o <select> en su Shadow DOM no se asocia automáticamente dentro del formulario que lo contiene. Los primeros usuarios de Web Component añadían campos ocultos al DOM de la página o utilizaban la interfaz FormData para actualizar los valores. Ninguno de los dos es particularmente práctico y rompe la encapsulación de Web Component.

La nueva interfaz ElementInternals permite que un componente web se enganche a los formularios para poder definir valores y validez personalizados. Está implementado en Chrome, pero hay un polyfill disponible para otros navegadores.

Para demostrarlo, crearás un componente básico <input-age name="your-age"></input-age> . La clase debe tener un valor estático formAssociated establecido como true y, opcionalmente, un método formAssociatedCallback() puede ser llamado cuando el formulario exterior se asocia:

// <input-age> web component
class InputAge extends HTMLElement {

  static formAssociated = true;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id);
  }

El constructor debe ejecutar ahora el método attachInternals(), que permite al componente comunicarse con el formulario y con otro código JavaScript que quiera inspeccionar el valor o la validación:

  constructor() {

    super();
    this.internals = this.attachInternals();
    this.setValue('');

  }

  // set form value

  setValue(v) {

    this.value = v;

    this.internals.setFormValue(v);

  }

El método setFormValue() de ElementInternal establece el valor del elemento para el formulario padre inicializado con una cadena vacía aquí (también se le puede pasar un objeto FormData con múltiples pares nombre/valor). Otras propiedades y métodos incluyen:

  • form: el formulario principal
  • labels: una matriz de elementos que etiquetan el componente
  • Opciones de la API de validación de restricciones como willValidate, checkValidity y validationMessage

El método connectedCallback() crea un Shadow DOM como antes, pero también debe monitorear el campo para los cambios, por lo que setFormValue() se puede ejecutar:

  connectedCallback() {

    const shadow = this.attachShadow({ mode: 'closed' });

    shadow.innerHTML = `
      <style>input { width: 4em; }</style>
      <input type="number" placeholder="age" min="18" max="120" />`;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value);
    });

  }

Ahora puedes crear un formulario HTML utilizando este componente web que actúa de forma similar a otros campos de formulario:

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Funciona, pero hay que reconocer que es un poco enrevesado.

Compruébalo en la demostración de CodePen

Para más información, consulta este artículo sobre controles de formulario más completos.

Resumen

Los Componentes Web han luchado para ganar acuerdo y adopción en un momento en que los frameworks de JavaScript han crecido en estatura y capacidad. Si vienes de React, Vue.js o Angular, los Components Web pueden parecer complejos y torpes, especialmente cuando faltan características como la vinculación de datos y la gestión de estados.

Hay que limar asperezas, pero el futuro de los Componentes Web es brillante. Son independientes del marco de trabajo, ligeros, rápidos y pueden implementar funcionalidades que serían imposibles de realizar solo con JavaScript.

Hace una década, pocos habrían abordado un sitio sin jQuery, pero los proveedores de navegadores tomaron las partes excelentes y añadieron alternativas nativas (como querySelector). Lo mismo ocurrirá con los frameworks de JavaScript, y Componentes Web es ese primer paso tentativo.

¿Tienes alguna duda sobre cómo utilizar los Componentes Web? Hablemos de ello en la sección de comentarios.

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.