Introducción a Shadow DOM

Introducción

Web Components es un conjunto de estándares de vanguardia que tienen las siguientes características:

  1. Permitir la compilación de widgets
  2. …que se pueden volver a usar de forma confiable
  3. Además, esto no provocará fallas en las páginas si la próxima versión del componente cambia los detalles de implementación internos.

¿Esto significa que debes decidir cuándo usar HTML o 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 Web Components están diseñados para ayudarte a hacerlo.

Sin embargo, hay 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 la hoja de estilo del documento podría aplicarse accidentalmente a partes dentro del widget, que tu código JavaScript podría modificar accidentalmente partes dentro del widget, que tus IDs podrían superponerse con los IDs 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 los componentes web usar. En este instructivo, se muestra cómo usar el DOM secundario.

Hola, Shadow World

Con Shadow DOM, los elementos pueden obtener un nuevo tipo de nodo asociado con ellos. Este nuevo tipo de nodo se denomina raíz en las sombras. Un elemento que tiene una raíz en las sombras asociada se denomina host en las sombras. No se renderiza el contenido de un host en sombra, sino el contenido de la raíz en sombra.

Por ejemplo, si tuvieras un marcado como este:

<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>

cómo 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 cuál es el textContent del botón, no se mostrará “こんlunちの影の世界!”, sino de “Hello, world!” porque el subárbol del DOM debajo de shadow root está encapsulado.

Separa el contenido de la presentación

Ahora, veremos cómo usar Shadow DOM para separar el contenido de la presentación. Supongamos que tenemos esta etiqueta de nombre:

<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>

Este es el lenguaje de marcado. Esto es lo que escribirías 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>

Debido a que el árbol del DOM carece de encapsulamiento, toda la estructura de la etiqueta de nombre se expone al documento. Si otros elementos de la página usan accidentalmente los mismos nombres de clase para aplicar estilos o escribir secuencias de comandos, tendremos problemas.

Podemos evitar que tengas un mal momento.

Paso 1: Oculta los detalles de la presentación

Semánticamente, es probable que solo nos interese lo siguiente:

  • Es una etiqueta con el nombre.
  • El nombre es “Roberto”.

Primero, escribimos un marcado que está más cerca de la semántica real que queremos:

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

Luego, colocamos todos los estilos y divs que se usan 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. Como movimos los elementos DOM de presentación dentro de un elemento <template>, no se renderizan, pero se puede acceder a ellos desde JavaScript. Ahora, hacemos eso 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 raíz de sombra, la etiqueta de nombre se vuelve a renderizar. Si haces clic con el botón derecho en la etiqueta personal e inspeccionas el elemento, notas que es lenguaje de marcado semántico dulce:

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

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

Paso 2: Separa el contenido de la presentación

Nuestra etiqueta de nombre ahora oculta los detalles de la presentación de la página, pero en realidad no separa la presentación del contenido, ya que, aunque el contenido (el nombre “Roberto”) está en la página, el nombre que se renderiza es el que copiamos en la raíz de la sombra. Si queremos cambiar el nombre en la etiqueta, tendríamos que hacerlo en dos lugares, y es posible que se desincronicen.

Los elementos HTML son composicionales, por ejemplo, puedes colocar un botón dentro de una tabla. Lo que necesitamos aquí es la composición: la etiqueta del nombre debe ser una composición del fondo rojo, el texto “Hola” y el contenido que está en la etiqueta del nombre.

Tú, el autor del componente, defines cómo funciona la composición con tu widget con 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 con cuidado el contenido del host paralelo para presentarlo en ese punto.

Si cambiamos el marcado en el 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 host en sombra se proyecta en el lugar en el que aparece el elemento <content>.

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

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

y eso es todo. El navegador actualiza automáticamente la renderización de la etiqueta de nombre porque proyectamos el contenido de la etiqueta en su lugar con <content>.

<div id="ex2b">

Ahora logramos separar el contenido y la presentación. El contenido está en el documento y la presentación está en el DOM sombreado. El navegador las mantiene sincronizadas automáticamente cuando llega el momento de renderizar algo.

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 nombre, ese código solo debe controlar una estructura simple que contiene un <div> en lugar de varios.

Ahora, si cambiamos nuestra presentación, no es necesario que cambiemos el código.

Por ejemplo, supongamos que queremos localizar nuestra etiqueta de nombre. 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 la raíz secundaria se mantiene igual. Solo lo que se coloca 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 actual en la Web, ya que el código de actualización de tu nombre puede depender de la estructura del componente, que es simple y coherente. Tu código de actualización de nombre no necesita conocer la estructura que se usa para la renderización. Si consideramos lo que se renderiza, el nombre aparece en segundo lugar en inglés (después de “Hola. My name is”), pero primero en japonés (antes de “と申します”). Esa distinción no tiene sentido 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 nombre no tiene que conocer ese detalle.

Crédito extra: Proyección avanzada

En el ejemplo anterior, el elemento <content> selecciona todo el contenido del host en la sombra. Con 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.

El motivo es que, como saben las personas que hackean Shadow DOM, la construcción del árbol de lo que realmente se renderiza en pantalla es como una gran fiesta. El elemento de contenido es la invitación que permite que el contenido del documento entre en la parte de renderización de Shadow DOM en el backstage. Estas invitaciones se envían en orden; la persona que la reciba depende de a quién se dirija (es decir, el atributo select). El contenido, una vez que se recibe la invitación, siempre acepta la invitación (¿quién no lo haría?) y desaparece. Si se vuelve a enviar una invitación posterior a esa dirección, no habrá nadie en casa y no llegará 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 ir a la parte azul. (Es posible que por eso esté tan azul, aunque la miseria ama la compañía, así que nunca se sabe).

Si no se invita a un elemento a ningún elemento, no se renderizará. Eso es lo que sucedió con el texto “Hola, mundo” en el primer ejemplo. Esto es útil cuando deseas lograr una renderización radicalmente diferente: escribe el modelo semántico en el documento, que es a lo que pueden acceder las secuencias de comandos de la página, pero ocúltalo para la renderización y conéctalo a un modelo de renderización realmente diferente en Shadow DOM con JavaScript.

Por ejemplo, HTML tiene un selector de fecha agradable. Si escribes <input type="date">, obtendrás un calendario emergente ordenado. Pero, ¿qué sucede si quieres permitir que el usuario elija un rango de fechas para sus vacaciones en la isla de postre (ya sabes… con hamacas hechas de Red Vines)? Configura tu documento de la siguiente manera:

<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>

pero crea un DOM sombreado que use una tabla para crear un calendario elegante que destaque el rango de fechas, etcétera. 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 van a renderizar? El motivo es que, si un usuario ve el formulario con un navegador que no es compatible con Shadow DOM, el formulario aún se puede usar, solo que no es tan atractivo. El usuario ve 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>

Aprobación del curso Shadow DOM 101

Esos son los conceptos básicos de Shadow DOM. ¡Aprobaste el curso introductorio! Puedes hacer más con Shadow DOM, por ejemplo, puedes usar varias sombras en un host de sombras, sombras anidadas para el encapsulamiento o diseñar tu página con vistas basadas en modelos (MDV) y Shadow DOM. Y los componentes web son más que solo Shadow DOM.

Explicaremos estos conceptos en publicaciones posteriores.