Domina el ciclo de vida de los elementos personalizados, la encapsulación del shadow DOM y la proyección de contenido mediante slots.
Abrir esta lección en KodokonLos 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.
class UserBadge extends HTMLElement {
static observedAttributes = ["name"];
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
this.textContent = this.getAttribute("name");
}
}
customElements.define("user-badge", UserBadge);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.
<template id="card-tpl">
<style>
:host { display: block; padding: 1rem; }
::slotted(h2) { margin: 0; }
</style>
<slot name="title"></slot>
<slot></slot>
</template>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.
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);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.
attachShadow({ mode: 'closed' }), ¿qué devuelve element.shadowRoot desde el exterior?null: solo la referencia devuelta por attachShadow da acceso al shadow root<slot>?