Kodokon kodokon.com

Web Components: elementos personalizados, shadow DOM y slots

Domina el ciclo de vida de los elementos personalizados, la encapsulación del shadow DOM y la proyección de contenido mediante slots.

12 min · 3 preguntas

Abrir esta lección en Kodokon

Los elementos personalizados (custom elements) extienden el vocabulario de HTML: cualquier clase que herede de HTMLElement puede registrarse en el CustomElementRegistry, siempre que su nombre contenga un guion (user-badge, nunca userbadge). Punto sutil: si la etiqueta aparece en el documento antes de que se llame a customElements.define, primero se crea como un elemento desconocido y luego se actualiza (upgrade) en su sitio - el constructor se invoca entonces a posteriori sobre un nodo ya presente en el árbol.

JAVASCRIPT
class UserBadge extends HTMLElement {
  static observedAttributes = ["name"];
  connectedCallback() {
    this.render();
  }
  attributeChangedCallback() {
    this.render();
  }
  render() {
    this.textContent = this.getAttribute("name");
  }
}
customElements.define("user-badge", UserBadge);
Un elemento personalizado mínimo con un ciclo de vida completo.

El ciclo de vida esconde algunas trampas: attributeChangedCallback solo se dispara para los atributos listados en observedAttributes, y durante una actualización se llama para cada atributo ya presente antes de connectedCallback. La especificación prohíbe que el constructor inspeccione atributos e hijos, o que añada alguno: en el momento en que se ejecuta (vía createElement o clonación), el elemento puede estar completamente vacío.

HTML
<template id="card-tpl">
  <style>
    :host { display: block; padding: 1rem; }
    ::slotted(h2) { margin: 0; }
  </style>
  <slot name="title"></slot>
  <slot></slot>
</template>
Una plantilla inerte: analizada, pero ni ejecutada ni renderizada.

attachShadow crea un subárbol encapsulado: los estilos no se filtran fuera de él, los selectores de la página no llegan a su interior - solo las propiedades CSS heredadas (color, font) y las propiedades personalizadas cruzan la frontera. El contenido de un <template> vive en un DocumentFragment accesible mediante .content: sus scripts no se ejecutan y sus imágenes no se cargan hasta que lo clonas en el documento. Los nodos colocados en el light DOM se proyectan en los elementos <slot>: permanecen físicamente en el light DOM, y el evento slotchange te notifica de cualquier cambio en la asignación.

JAVASCRIPT
class InfoCard extends HTMLElement {
  constructor() {
    super();
    const tpl = document.getElementById("card-tpl");
    this.attachShadow({ mode: "open" })
      .appendChild(tpl.content.cloneNode(true));
  }
}
customElements.define("info-card", InfoCard);
Clonando la plantilla en un shadow root abierto.

Dos extensiones merecen tu atención. Elementos personalizados asociados a formularios (form-associated custom elements): con static formAssociated = true y attachInternals(), tu componente participa plenamente en los formularios (valor enviado, validación nativa, estados :invalid). Y el shadow DOM declarativo: <template shadowrootmode=open> adjunta un shadow root en el momento del análisis, sin JavaScript, lo que hace que los web components sean compatibles con el renderizado del lado del servidor.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué el constructor de un elemento personalizado no debe leer sus atributos ni crear hijos?
    • Porque los atributos están congelados como solo lectura antes de connectedCallback
    • Porque el shadow DOM oculta el light DOM durante la construcción
    • Porque en ese momento (createElement, clonación) el elemento puede estar vacío; la especificación exige un constructor sin efecto sobre el contenido
  2. Con attachShadow({ mode: 'closed' }), ¿qué devuelve element.shadowRoot desde el exterior?
    • null: solo la referencia devuelta por attachShadow da acceso al shadow root
    • El shadow root, pero de solo lectura
    • Un DocumentFragment vacío
    • Se lanza una DOMException
  3. ¿Dónde viven físicamente los nodos proyectados en un <slot>?
    • Se mueven al shadow DOM en el momento del renderizado
    • Permanecen en el light DOM; el slot simplemente los muestra en su posición dentro del árbol aplanado
    • Se clonan: el original permanece en el light DOM, la copia vive en el shadow DOM