Shadow DOM, plantillas y ranuras

Funciones web: Shadow DOM <slot>

Una raíz de sombra es un subárbol del DOM encapsulado que se adjunta a un elemento host: los estilos que contiene no se filtran hacia afuera y los estilos de la página no se filtran hacia adentro (excepto las propiedades heredadas). Las plantillas construyen el árbol de manera eficiente, y las ranuras proyectan el contenido del DOM de luz en él.

Cómo adjuntar una raíz secundaria

this.attachShadow({ mode: 'open', delegatesFocus: true });

delegatesFocus: true hace que, cuando se enfoca el host, el enfoque se mueva al primer elemento enfocable dentro del árbol de shadow, enruta el diseño de :focus al host y, en los elementos personalizados asociados a formularios (static formAssociated = true), reenvía los clics de <label> al control interno. Establécelo siempre que el componente contenga controles enfocables. Usa mode: 'open' en casi todos los casos para que las herramientas para desarrolladores, las secuencias de comandos de la página y la hidratación del servidor puedan llegar a element.shadowRoot (consulta Orientación de componentes web).

Plantillas: Construye el árbol de manera eficiente

Define un <template> una vez (alcance del módulo o un campo estático) y clónalo por instancia. Clonar un fragmento analizado es mucho más rápido que volver a analizar una cadena de innerHTML en cada construcción.

const template = document.createElement('template');
template.innerHTML = `
  <div class="card">
    <slot name="header"></slot>
    <div class="body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
`;

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // MANDATORY: deep-clone the template content so each instance gets its own nodes.
    this.shadowRoot.append(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

Mantén las plantillas optimizadas. La "sopa de div" con un nivel de anidamiento profundo y muchos niveles de raíces de shadow anidadas agregan un costo medible de resolución de estilo y diseño, lo que genera un gran impacto en el rendimiento a gran escala. Prefiere una estructura plana y elementos semánticos.

Espacios: Las partes que sorprenden a las personas

Los espacios predeterminados y con nombre funcionan como se documenta; estos son los comportamientos que pueden causar problemas:

  • Los espacios en blanco cuentan. Los nodos de texto entre elementos (incluidos los espacios en blanco puros) se asignan al espacio predeterminado, por lo que un espacio predeterminado rara vez está realmente "vacío".
  • Se conserva el orden del DOM, no el de los espacios publicitarios. Los nodos proyectados mantienen su orden original del DOM claro para los fines de :first-child/:last-child, independientemente de la ranura que ocupen. Una regla ::slotted(*:first-child) se orienta al primer elemento secundario del DOM, no al primer elemento secundario de ese espacio, por lo que, a menudo, no coincidirá con lo que esperas.
  • slotchange es el indicador de tiempo de actualización. Se activa cuando cambian los nodos asignados (incluida la asignación inicial) y es la forma confiable de saber que los elementos secundarios del DOM ligero llegaron después de una actualización, cuando es posible que no hayan existido durante connectedCallback.
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

Carga hojas de estilo en una raíz de sombra

Método Ventajas Desventajas
<style> en la plantilla Simple, completamente encapsulado, funciona sin JS. Se duplica por instancia, lo que aumenta el tamaño del DOM a gran escala.
<link rel="stylesheet"> Es un archivo externo almacenable en caché. Solicitud adicional; riesgo de un destello de contenido sin diseño.
adoptedStyleSheets Un CSSStyleSheet compartido entre todas las instancias; es el más rápido a gran escala. Requiere JS; las hojas construidas no admiten @import.

Para muchas instancias de un componente, prefiere una hoja de estilo construida compartida:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // The same sheet object is reused by every instance; no per-instance parse.
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}