Introducción a Shadow DOM

Introducción

Web Components es un conjunto de estándares de vanguardia que hacen lo siguiente:

  1. Permite compilar widgets
  2. ... que se pueden volver a usar de manera confiable
  3. ... y que no dañará las páginas si la siguiente versión del componente cambia los detalles de implementación internos.

¿Esto significa que debes decidir cuándo usar HTML/JavaScript y cuándo usar componentes web? ¡No! HTML y JavaScript pueden crear elementos visuales interactivos. Los widgets son elementos visuales interactivos. Tiene sentido aprovechar tus habilidades de HTML y JavaScript cuando desarrollas un widget. Los estándares de componentes web están diseñados para ayudarte a hacerlo.

Sin embargo, existe un problema fundamental que dificulta el uso de los widgets compilados con HTML y JavaScript: el árbol del DOM dentro de un widget no se encapsula del resto de la página. Esta falta de encapsulamiento significa que tu hoja de estilo del documento podría aplicarse por error a partes dentro del widget; tu JavaScript podría modificar por accidente partes dentro del widget; tus ID podrían superponerse con los ID dentro del widget, etcétera.

Los componentes web se componen de tres partes:

  1. Plantillas
  2. Shadow DOM
  3. Elementos personalizados

Shadow DOM soluciona el problema de encapsulamiento del árbol del DOM. Las cuatro partes de los componentes web están diseñadas para funcionar en conjunto, pero también puedes elegir qué partes de estos componentes deseas usar. En este instructivo, se muestra cómo usar Shadow DOM.

Hello World Shadow World

Con Shadow DOM, los elementos pueden obtener un nuevo tipo de nodo asociado con ellos. Este nuevo tipo de nodo se denomina shadow root. Un elemento que tiene una shadow root asociada a él se denomina host de sombras. No se renderiza el contenido de un shadow host. En su lugar, se renderiza el contenido de la shadow root.

Por ejemplo, si tuvieras un lenguaje de marcado como el siguiente:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

en lugar de

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

se ve tu página

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

No solo eso, si JavaScript en la página pregunta qué es el textContent del botón, no se encontrará “

Separar el contenido de la presentación

Ahora, veremos el uso de Shadow DOM para separar el contenido de la presentación. Supongamos que tenemos esta etiqueta de identificación:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Esta es la marca. Esto es lo que escribirás hoy. No usa Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Como el árbol del DOM no tiene encapsulamiento, se expone toda la estructura de la etiqueta de nombre en el documento. Si otros elementos de la página usan accidentalmente los mismos nombres de clase para el diseño o la escritura de secuencias de comandos, la vamos a divertir.

Podemos evitar tener un mal momento.

Paso 1: Oculta los detalles de la presentación

Semánticamente, probablemente solo nos interese lo siguiente:

  • Es una etiqueta personal.
  • El nombre es “Bob”.

Primero, escribimos un lenguaje de marcado que se acerque más a la semántica real que deseamos:

<div id="nameTag">Bob</div>

Luego, colocamos todos los estilos y elementos div usados para la presentación en un elemento <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

En este punto, "Bob" es lo único que se renderiza. Dado que movemos los elementos de presentación del DOM dentro de un elemento <template>, no se renderizan, pero se puede acceder a ellos desde JavaScript. Lo hacemos ahora para propagar la shadow root:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Ahora que configuramos una shadow root, se volverá a renderizar la etiqueta personal. Si haces clic con el botón derecho en la etiqueta de identificación y, luego, inspeccionas el elemento, verás que es un lenguaje de marcado semántico atractivo:

<div id="nameTag">Bob</div>

Esto demuestra que, con Shadow DOM, ocultamos los detalles de presentación de la etiqueta personal del documento. Los detalles de la presentación se encapsulan en el Shadow DOM.

Paso 2: Separa el contenido de la presentación

Nuestra etiqueta personal ahora oculta los detalles de la presentación de la página, pero en realidad no separa la presentación del contenido porque, si bien el contenido (el nombre "Bob") está en la página, el nombre que se renderiza es el que copiamos en la shadow root. Si quisiéramos cambiar el nombre de la etiqueta de identificación, tendríamos que hacerlo en dos lugares y es posible que se desincronizan.

Los elementos HTML son composiciones: puedes colocar un botón dentro de una tabla, por ejemplo. En este caso, la composición es lo que necesitamos: la etiqueta personal debe ser una composición del fondo rojo, el texto "Hi!" y el contenido de la etiqueta personal.

Tú, el autor del componente, defines cómo funciona la composición con tu widget mediante un elemento nuevo llamado <content>. Esto crea un punto de inserción en la presentación del widget, y el punto de inserción elige muy bien el contenido del host paralelo para presentarlo en ese punto.

Si cambiamos el lenguaje de marcado en Shadow DOM a lo siguiente:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Cuando se renderiza la etiqueta de nombre, el contenido del shadow host se proyecta en el lugar en el que aparece el elemento <content>.

Ahora la estructura del documento es más simple porque el nombre está en un solo lugar: el documento. Si tu página necesita actualizar el nombre del usuario, solo debes escribir lo siguiente:

document.querySelector('#nameTag').textContent = 'Shellie';

Eso es todo. El navegador actualiza automáticamente la renderización de la etiqueta de nombre, ya que proyectamos el contenido de la etiqueta en el lugar con <content>.

<div id="ex2b">

Ahora hemos logrado la separación del contenido y la presentación. El contenido está en el documento; la presentación está en Shadow DOM. El navegador los mantiene sincronizados automáticamente cuando llega el momento de renderizar un elemento.

Paso 3: Ganancias

Si separamos el contenido y la presentación, podemos simplificar el código que manipula el contenido; en el ejemplo de la etiqueta de identificación, ese código solo necesita lidiar con una estructura simple que contiene un <div> en lugar de varios.

Ahora, si cambiamos nuestra presentación, no necesitamos cambiar nada del código.

Por ejemplo, supongamos que queremos localizar nuestra etiqueta de identificación. Sigue siendo una etiqueta de nombre, por lo que el contenido semántico del documento no cambia:

<div id="nameTag">Bob</div>

El código de configuración de shadow root sigue siendo el mismo. Lo que se pone en la raíz de la sombra cambia:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Esta es una gran mejora con respecto a la situación en la Web actual, ya que tu código de actualización de nombre puede depender de la estructura del componente, que es simple y coherente. No es necesario que el código de actualización de nombre conozca la estructura que se usa para el procesamiento. Si consideramos lo que se renderiza, el nombre aparece segundo en inglés (después de “Hi! Mi nombre es”), pero primero en japonés (antes "と申Historyます"). Esa distinción no tiene sentido desde el punto de vista semántico desde el punto de vista de la actualización del nombre que se muestra, por lo que el código de actualización de nombres no necesita conocer ese detalle.

Crédito adicional: Proyección avanzada

En el ejemplo anterior, el elemento <content> elige cuidadosamente todo el contenido del host paralelo. Mediante el atributo select, puedes controlar lo que proyecta un elemento de contenido. También puedes usar varios elementos de contenido.

Por ejemplo, si tienes un documento que contiene lo siguiente:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

y una shadow root que usa selectores CSS para seleccionar contenido específico:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

El elemento <div class="email"> coincide con los elementos <content select="div"> y <content select=".email">. ¿Cuántas veces aparece la dirección de correo electrónico de Roberto, y en qué colores?

La respuesta es que la dirección de correo electrónico de Roberto aparece una vez y es amarilla.

Esto se debe a que, como saben las personas que hackean Shadow DOM, construir el árbol de lo que se renderiza en la pantalla es como una gran fiesta. El elemento de contenido es la invitación que permite que el contenido del documento llegue al grupo de renderización del Shadow DOM detrás de escena. Estas invitaciones se entregan en orden. La persona que recibe una invitación depende de a quién está dirigida (es decir, el atributo select). Una vez que el contenido recibe la invitación, siempre la acepta (¿quién no?) y sale. Si se te vuelve a enviar una invitación a esa dirección, significa que no hay nadie en casa y no llega a tu grupo.

En el ejemplo anterior, <div class="email"> coincide con el selector div y el selector .email, pero como el elemento de contenido con el selector div aparece antes en el documento, <div class="email"> va a la parte amarilla y nadie está disponible para pasar a la parte azul. (Podría ser por qué es tan azul, aunque la miseria ama la compañía, así que nunca se sabe).

Si se invita a un evento no a ningún grupo, no se renderizará. Eso es lo que sucedió con el texto "Hello, World" en el primer ejemplo. Esto resulta útil cuando deseas lograr un procesamiento muy diferente: escribe el modelo semántico en el documento, que es lo que pueden acceder las secuencias de comandos en la página, pero ocúltalo con fines de renderización y conéctalo a un modelo de renderización realmente diferente en Shadow DOM mediante JavaScript.

Por ejemplo, HTML tiene un buen selector de fecha. Si escribes <input type="date">, se muestra un calendario emergente genial. Pero ¿qué sucede si quieres permitir que el usuario elija un rango de fechas para sus vacaciones en el desierto (ya sabes... con hamacas hechas de vides rojas)? Para configurar el documento, debes hacer lo siguiente:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

sino que crea un Shadow DOM que usa una tabla para crear un calendario elegante que destaque el rango de fechas, y así sucesivamente. Cuando el usuario hace clic en los días del calendario, el componente actualiza el estado en las entradas startDate y endDate. Cuando el usuario envía el formulario, se envían los valores de esos elementos de entrada.

¿Por qué incluí etiquetas en el documento si no se renderizarán? Esto se debe a que, si un usuario ve el formulario con un navegador que no admite Shadow DOM, este se puede usar de todos modos, pero no tan atractivo. El usuario verá algo como lo siguiente:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Pasas Shadow DOM 101

Esos son los conceptos básicos de Shadow DOM: ¡Pasas Shadow DOM 101! Puedes hacer mucho más con Shadow DOM, por ejemplo, puedes usar varias sombras en un shadow host o sombras anidadas para el encapsulamiento, o bien diseñar tu página con vistas controladas por modelos (MDV) y Shadow DOM. Los componentes web son más que solo Shadow DOM.

Explicaremos esto en publicaciones posteriores.