Plantilla, ranura y sombra

El beneficio de los componentes web es que se pueden reutilizar: puedes crear un widget de IU una vez y reutilizarlo varias veces. Mientras que necesitas JavaScript para crear componentes web, no necesitas una biblioteca de JavaScript. HTML y las APIs asociadas proporcionan todo lo que necesitas.

El estándar de componentes web consta de tres partes: plantillas HTML, Elementos personalizados y Shadow DOM. Cuando se combinan, permiten crear elementos personalizados, autónomos (encapsulados) y reutilizables que se pueden integrar a la perfección. en aplicaciones existentes, como todos los demás elementos HTML que hemos abarcado.

En esta sección, crearemos el elemento <star-rating>, un componente web que permite a los usuarios calificar una experiencia en una escala de una a cinco estrellas. Cuando se le asigna un nombre a un elemento personalizado, se recomienda usar solo letras minúsculas. Además, incluye un guion, ya que ayuda a distinguir entre los elementos normales y personalizados.

Analizaremos cómo usar los elementos <template> y <slot>, el atributo slot y JavaScript para crear una plantilla con un Shadow DOM encapsulado. Luego, reutilizaremos el elemento definido y personalizaremos una sección de texto como lo harías con cualquier elemento o componente web. También analizaremos brevemente el uso de CSS desde y fuera del elemento personalizado.

El elemento <template>

El elemento <template> se usa para declarar fragmentos de HTML que se clonarán e insertarán en el DOM con JavaScript. El contenido del elemento no se renderiza de forma predeterminada. En cambio, se crean instancias de ellas con JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Como el contenido de un elemento <template> no se escribe en la pantalla, <form> y su contenido no se renderizan. Sí, este códec está vacío, pero si inspeccionas la pestaña HTML, verás el lenguaje de marcado <template>.

En este ejemplo, <form> no es un elemento secundario de <template> en el DOM. En cambio, el contenido de los elementos <template> es secundario de un DocumentFragment mostrado por HTMLTemplateElement.content propiedad. Para que sea visible, se debe usar JavaScript para tomar el contenido y agregarlo al DOM.

Este código breve de JavaScript no creó un elemento personalizado. En cambio, en este ejemplo, se agregó el contenido de <template> a <body>. El contenido ahora forma parte del DOM visible y con estilo.

Captura de pantalla del códec anterior como se muestra en el DOM.

No es muy útil solicitar JavaScript para implementar una plantilla solo para una calificación por estrellas, pero crear un componente web para una se usa repetidamente, el widget personalizable de calificación por estrellas es útil.

El elemento <slot>

Incluimos un espacio para incluir una leyenda personalizada por caso. HTML proporciona un <slot> como marcador de posición dentro de un <template> que, si se proporciona un nombre, crea un "ranura con nombre". Se puede usar una ranura con nombre para personalizar el contenido dentro de un componente web. El elemento <slot> nos permite controlar el origen de los elementos secundarios de una elemento debe insertarse dentro de su shadow tree.

En nuestra plantilla, cambiamos <legend> por <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

El atributo name se usa para asignar ranuras a otros elementos si el elemento tiene un atributo slot cuyo valor coincide con el el nombre de una ranura con nombre. Si el elemento personalizado no tiene coincidencias para un espacio, se renderizará el contenido de <slot>. Por lo tanto, incluimos un <legend> con contenido genérico que se puede procesar si alguien simplemente incluye <star-rating></star-rating>, sin contenido, en su HTML.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

El atributo slot es un atributo global que se usa para reemplazar el contenido de <slot> dentro de un <template>. En nuestro elemento personalizado, el elemento con el atributo de espacio es un <legend>. No tiene por qué serlo. En nuestra plantilla, <slot name="star-rating-legend"> se reemplazará por <anyElement slot="star-rating-legend">, donde <anyElement> puede ser cualquier elemento, incluso otro elemento personalizado.

Elementos sin definir

En nuestra <template>, usamos un elemento <rating>. Este no es un elemento personalizado. Más bien, es un elemento desconocido. Navegadores no fallan cuando no reconocen un elemento. El navegador trata los elementos HTML no reconocidos como intercalados anónimos. elementos a los que se les puede aplicar estilo con CSS. Al igual que <span>, los elementos <rating> y <star-rating> no tienen un usuario-agente aplicado. estilos o semánticas.

Ten en cuenta que <template> y el contenido no se renderizan. El <template> es un elemento conocido que incluye contenido que no debe renderizarse. Aún no se definió el elemento <star-rating>. Hasta que definamos un elemento, el navegador lo mostrará como todos los elementos no reconocidos. Por ahora, el elemento <star-rating> no reconocido se trata como un elemento intercalado anónimo, por lo que el contenido incluidas las leyendas y la <p> del tercer <star-rating> se muestran como se mostrarían si estuvieran en un <span>.

Definamos nuestro elemento para convertir este elemento no reconocido en un elemento personalizado.

Elementos personalizados

Se requiere JavaScript para definir elementos personalizados. Cuando se define, el contenido del elemento <star-rating> se reemplaza por un elemento shadow root que contiene todos los contenidos de la plantilla que asociamos con ella. Se reemplazan los elementos <slot> de la plantilla con el contenido del elemento dentro de <star-rating> cuyo valor de atributo slot coincida con el valor de nombre de <slot>, si solo hay uno. De lo contrario, se muestra el contenido de las ranuras de la plantilla.

En un elemento personalizado, el contenido de un elemento personalizado que no está asociado a un espacio (el <p>Is this text visible?</p> de nuestro tercer <star-rating>) no se incluye en la shadow root y, por lo tanto, no se muestra.

Definimos el elemento personalizado llamado star-rating. extendiendo HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Ahora que el elemento está definido, cada vez que el navegador encuentre un elemento <star-rating>, se renderizará como se definió por el elemento con #star-rating-template, que es nuestra plantilla. El navegador adjuntará un árbol del shadow DOM al nodo y anexar es una clonación del contenido de la plantilla en ese shadow DOM. Ten en cuenta que los elementos sobre los que puedes attachShadow() son limitados.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Si observas las herramientas para desarrolladores, notarás que <form> de <template> forma parte de la raíz secundaria de cada elemento personalizado. Una clonación del contenido de <template> es visible en cada elemento personalizado en las herramientas para desarrolladores y visible en el navegador, pero del elemento personalizado en sí no se renderizan en la pantalla.

Captura de pantalla de Herramientas para desarrolladores en la que se muestra el contenido de la plantilla clonada en cada elemento personalizado.

En el ejemplo de <template>, agregamos el contenido de la plantilla al cuerpo del documento y lo agregamos al DOM normal. En la definición de customElements, usamos lo mismo appendChild(), pero el contenido de la plantilla clonada se agregó a una shadow DOM encapsulado.

¿Notas que las estrellas volvieron a ser botones de selección sin estilo? Por ser parte de un shadow DOM y no de un DOM estándar, no se aplica el estilo de la pestaña CSS de Codepen. El CSS de esa pestaña los estilos se definen en el documento y no en el shadow DOM, por lo que no se aplican. Tenemos que crear conjuntos de datos para definir el estilo de nuestro contenido de Shadow DOM encapsulado.

Shadow DOM

El Shadow DOM establece el alcance de los estilos CSS en cada shadow tree y lo aísla del resto del documento. Esto significa que los CSS externos no se aplica a tu componente, y los estilos de componente no tienen efecto en el resto del documento, a menos que intencionadamente dirigirlos.

Como agregamos el contenido a un shadow DOM, podemos incluir un elemento <style>. lo que proporciona CSS encapsulada al elemento personalizado.

Cuando se limita el alcance del elemento personalizado, no tenemos que preocuparnos de que los estilos se filtren en el resto del documento. Podemos reducir sustancialmente la especificidad de los selectores. Por ejemplo, como las únicas entradas que se usan en el elemento personalizado son los botones de botones, podemos usar input en lugar de input[type="radio"] como selector.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Mientras que los componentes web se encapsulan con lenguaje de marcado en <template>, y los estilos CSS se definen en el shadow DOM y se ocultan de todo lo que está fuera de los componentes, el contenido del espacio que se renderiza, la <anyElement slot="star-rating-legend"> de la <star-rating> no está encapsulada.

Un diseño fuera del alcance actual

Es posible, aunque no sencillo, darle estilo al documento desde un shadow DOM y aplicar ajustes de estilo al contenido de un shadow DOM a partir del los estilos globales. El límite de las sombras, donde termina el shadow DOM y comienza el DOM normal, se puede recorrer, pero solo de forma muy intencional.

El shadow tree es el árbol del DOM dentro del shadow DOM. La shadow root es el nodo raíz del shadow tree.

La pseudoclase :host selecciona el <star-rating>, el elemento del host de sombra. El shadow host es el nodo del DOM al que se adjunta el shadow DOM. Para establecer la segmentación solo a versiones específicas del host, usa :host(). Esto seleccionará solo los elementos del host paralelo que coincidan con el parámetro que se pasó, como un selector de clase o atributo. Para seleccionar todos los elementos personalizados, puedes usar star-rating { /* styles */ } en el CSS global o :host(:not(#nonExistantId)) en los estilos de plantilla. En términos de especificidad, el CSS global gana.

El seudoelemento ::slotted() cruza el límite del shadow DOM. desde el shadow DOM. Selecciona un elemento con ranuras si coincide con el selector. En nuestro ejemplo, ::slotted(legend) coincide con nuestras tres leyendas.

Para apuntar a un shadow DOM de CSS en el alcance global, es necesario editar la plantilla. La part puedes agregar a cualquier elemento al que desees aplicar diseño. Luego, usa el seudoelemento ::part(). para hacer coincidir elementos dentro de un shadow tree que coinciden con el parámetro pasado. El ancla u elemento de origen del seudoelemento es el nombre del host o del elemento personalizado, en este caso, star-rating. El parámetro es el valor del atributo part.

Si el lenguaje de marcado de nuestra plantilla comenzara así:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Podríamos orientar a <form> y <fieldset> con lo siguiente:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Los nombres de las partes actúan de forma similar a las clases: un elemento puede tener varios nombres de partes separados por espacios y varios elementos pueden tienen el mismo nombre de pieza.

Google tiene una lista de tareas fantástica para crear elementos personalizados. También puedes aprender sobre los shadow DOM declarativos.

Verifica tus conocimientos

Pon a prueba tus conocimientos sobre plantillas, ranuras y sombras.

De forma predeterminada, los estilos externos al shadow DOM darán estilo a los elementos que están adentro.

Verdadero
Vuelve a intentarlo.
Falso
Correcto.

¿Cuál de las siguientes respuestas es una descripción correcta del elemento <template>?

Elemento genérico que se usa para mostrar cualquier contenido en tu página.
Vuelve a intentarlo.
Un elemento marcador de posición.
Vuelve a intentarlo.
Elemento que se usa para declarar fragmentos de HTML, que no se renderizarán de forma predeterminada.
Correcto.