Shadow DOM 301

Conceptos avanzados y APIs de DOM

En este artículo, se analizan más de las cosas increíbles que puedes hacer con el Shadow DOM. Se basa en los conceptos que se analizan en Shadow DOM 101 y Shadow DOM 201.

Usa varias raíces de sombra

Si organizas una fiesta, el ambiente se vuelve sofocante si todos están amontonados en la misma habitación. Quieres tener la opción de distribuir grupos de personas en varias salas. Los elementos que alojan el Shadow DOM también pueden hacer esto, es decir, pueden alojar más de una raíz de sombra a la vez.

Veamos qué sucede si intentamos adjuntar varias raíces de shadow DOM a un host:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

Lo que se renderiza es "Root 2 FTW", a pesar de que ya habíamos adjuntado un árbol de sombra. Esto se debe a que se aplica el último árbol de sombra que se agrega a un host. Es una pila LIFO en lo que respecta a la renderización. Si examinas las Herramientas para desarrolladores, podrás verificar este comportamiento.

Entonces, ¿cuál es el punto de usar varias sombras si solo se invita a la última a la renderización? Ingresa puntos de inserción de sombra.

Puntos de inserción de anuncios de prueba

Los "puntos de inserción de sombra" (<shadow>) son similares a los puntos de inserción normales (<content>) en el sentido de que son marcadores de posición. Sin embargo, en lugar de ser marcadores de posición para el contenido de un host, son hosts para otros árboles de sombra. Es el origen del Shadow DOM.

Como probablemente te imaginas, las cosas se complican a medida que profundizas en el tema. Por este motivo, la especificación es muy clara sobre lo que sucede cuando hay varios elementos <shadow> en juego:

Si volvemos a nuestro ejemplo original, la primera sombra root1 se dejó fuera de la lista de invitaciones. Si agregas un punto de inserción <shadow>, se restablecerá:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

Hay un par de aspectos interesantes sobre este ejemplo:

  1. "Root 2 FTW" se sigue renderizando por encima de "Root 1 FTW". Esto se debe a la ubicación del punto de inserción <shadow>. Si quieres lo contrario, mueve el punto de inserción: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Observa que ahora hay un punto de inserción <content> en root1. Esto hace que el nodo de texto "Light DOM" se incluya en la renderización.

¿Qué se renderiza en <shadow>?

A veces, es útil conocer el árbol de sombra más antiguo que se renderiza en un <shadow>. Puedes obtener una referencia a ese árbol a través de .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

Cómo obtener la raíz de sombra de un host

Si un elemento aloja un Shadow DOM, puedes acceder a su raíz secundaria más reciente con .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Si te preocupa que las personas crucen tus sombras, redefine .shadowRoot para que sea nulo:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

Es un poco un truco, pero funciona. En última instancia, es importante recordar que, si bien es increíblemente fantástico, el Shadow DOM no se diseñó para ser una función de seguridad. No dependas de él para aislar el contenido por completo.

Cómo compilar un Shadow DOM en JS

Si prefieres compilar el DOM en JS, HTMLContentElement y HTMLShadowElement tienen interfaces para eso.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

Este ejemplo es casi idéntico al de la sección anterior. La única diferencia es que ahora uso select para extraer el <span> que se agregó recientemente.

Cómo trabajar con puntos de inserción

Los nodos que se seleccionan fuera del elemento host y se "distribuyen" en el árbol de sombra se llaman… redoble de tambores… ¡nodos distribuidos! Se les permite cruzar el límite de sombra cuando los puntos de inserción los invitan a entrar.

Lo que resulta conceptualmente extraño de los puntos de inserción es que no mueven físicamente el DOM. Los nodos del host permanecen intactos. Los puntos de inserción solo vuelven a proyectar nodos del host en el árbol de sombra. Es un tema de presentación o renderización: "Mueve estos nodos aquí" "Renderiza estos nodos en esta ubicación".

Por ejemplo:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

¡Listo! El h2 no es un elemento secundario del DOM de sombra. Esto nos lleva a otro dato interesante:

Element.getDistributedNodes()

No podemos recorrer un <content>, pero la API de .getDistributedNodes() nos permite consultar los nodos distribuidos en un punto de inserción:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

Al igual que con .getDistributedNodes(), puedes llamar a .getDestinationInsertionPoints() para verificar en qué puntos de inserción se distribuye un nodo:

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

Herramienta: Shadow DOM Visualizer

Comprender la magia negra que es el Shadow DOM es difícil. Recuerdo que intenté comprenderlo por primera vez.

Para ayudarte a visualizar cómo funciona la renderización del Shadow DOM, creé una herramienta con d3.js. Ambos cuadros de lenguaje de marcado del lado izquierdo se pueden editar. No dudes en pegar tu propio lenguaje de marcado y experimentar para ver cómo funcionan las cosas y cómo los puntos de inserción intercambian los nodos de host en el árbol de sombra.

Visualizador de Shadow DOM
Iniciar el visualizador de Shadow DOM

Pruébalo y danos tu opinión.

Modelo de eventos

Algunos eventos cruzan el límite de sombra y otros no. En los casos en que los eventos cruzan el límite, el destino del evento se ajusta para mantener la encapsulación que proporciona el límite superior de la raíz de la sombra. Es decir, los eventos se reorientan para que parezca que provienen del elemento host en lugar de los elementos internos del Shadow DOM.

Reproducir acción 1

  • Este es interesante. Deberías ver un mouseout desde el elemento host (<div data-host>) hasta el nodo azul. Aunque sea un nodo distribuido, sigue estando en el host, no en el ShadowDOM. Si mueves el mouse más abajo, hacia el amarillo, se vuelve a producir un mouseout en el nodo azul.

Reproducir acción 2

  • Hay un mouseout que aparece en el host (al final). Normalmente, verías que se activan eventos mouseout para todos los bloques amarillos. Sin embargo, en este caso, estos elementos son internos del Shadow DOM y el evento no se propaga a través de su límite superior.

Play Action 3

  • Observa que, cuando haces clic en la entrada, el focusin no aparece en la entrada, sino en el nodo host. Se reorientó.

Eventos que siempre se detienen

Los siguientes eventos nunca cruzan el límite de sombra:

  • anular
  • error
  • seleccionar
  • cambiar
  • load
  • restablecer
  • resize
  • scroll
  • selectstart

Conclusión

Espero que estés de acuerdo en que el Shadow DOM es increíblemente potente. Por primera vez, tenemos una encapsulación adecuada sin el peso adicional de los <iframe> ni otras técnicas más antiguas.

Sin duda, el Shadow DOM es una bestia compleja, pero vale la pena agregarla a la plataforma web. Dedícale un tiempo. Apréndelo Haz preguntas.

Si quieres obtener más información, consulta el artículo de introducción de Dominic Shadow DOM 101 y mi artículo Shadow DOM 201: CSS & Styling.