Shadow DOM 301

Conceptos avanzados y APIs de DOM

Eric Bidelman

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

Cómo usar varias shadow roots

Si organizas una fiesta, puede ser incómodo si todos están en la misma habitación. Quieres la opción de distribuir grupos de personas en varias salas. Los elementos que alojan el DOM en las sombras también pueden hacer esto, es decir, pueden alojar más de una raíz en las sombras a la vez.

Veamos qué sucede si intentamos conectar varias raíces de sombra 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 sombras. Esto se debe a que el último árbol de sombras agregado a un host gana. Es una pila LIFO en lo que respecta a la renderización. Si examinas DevTools, se verifica este comportamiento.

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

Puntos de inserción de sombras

Los "puntos de inserción de sombras" (<shadow>) son similares a los puntos de inserción (<content>) normales en 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 sombras. ¡Es el principio de Shadow DOM!

Como probablemente te imaginarás, a medida que profundices en el agujero del conejo, todo se complica. Por este motivo, la especificación es muy clara sobre lo que sucede cuando están en juego varios elementos <shadow>:

Si volvemos a nuestro ejemplo original, el primer root1 en sombra se dejó fuera de la lista de invitaciones. Si agregas un punto de inserción <shadow>, lo harás de nuevo:

<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 algunos aspectos interesantes en este ejemplo:

  1. "Root 2 FTW" aún se renderiza por encima de "Root 1 FTW". Esto se debe a la ubicación en la que colocamos el 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 "DOM ligero" se incluya en el viaje de renderización.

¿Qué se renderiza en <shadow>?

A veces, es útil conocer el shadow tree 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 Shadow DOM, puedes acceder a su shadow root 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 se crucen entre tus sombras, redefine .shadowRoot para que sea nulo:

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

Es un poco hack, pero funciona. En definitiva, es importante recordar que, si bien es increíblemente fantástico, Shadow DOM no se diseñó para ser una función de seguridad. No confíes en él para lograr un aislamiento completo del contenido.

Cómo compilar Shadow DOM en JS

Si prefieres compilar 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> agregado recientemente.

Cómo trabajar con puntos de inserción

Los nodos que se seleccionan del elemento host y se “distribuyen” en el árbol de sombras se llaman…redoble de tambores…nodos distribuidos. Pueden cruzar el límite de la sombra cuando los puntos de inserción los invitan.

Lo que es conceptualmente extraño acerca 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 sombras. Se trata de una 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! h2 no es un elemento secundario del shadow DOM. Esto nos lleva a otro dato interesante:

Element.getDistributedNodes()

No podemos atravesar 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 verificar en qué puntos de inserción se distribuye un nodo llamando a su .getDestinationInsertionPoints():

<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: Visualizador de Shadow DOM

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

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

Visualizador de Shadow DOM
Cómo iniciar el Visualizador de Shadow DOM

Pruébala y danos tu opinión.

Modelo de eventos

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

Play Action 1

  • Esta es interesante. Deberías ver un mouseout del elemento host (<div data-host>) al nodo azul. Aunque es un nodo distribuido, sigue estando en el host, no en el ShadowDOM. Si colocas el cursor más abajo en el amarillo, se genera un mouseout en el nodo azul.

Acción de juego 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.

Reproduce la acción 3

  • Observa que, cuando haces clic en la entrada, el focusin no aparece en la entrada, sino en el nodo host. ¡Ya volvimos a atacar!

Eventos que siempre se detienen

Los siguientes eventos nunca cruzan el límite de la 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, contamos con un encapsulamiento adecuado sin el equipaje adicional de <iframe> ni otras técnicas más antiguas.

Sin lugar a dudas, el asunto de los shadow DOM es complejo, pero vale la pena agregarlo a la plataforma web. Dedícale tiempo. Aprende a usarlo. Haz preguntas.

Si quieres obtener más información, consulta el artículo introductorio de Dominic Shadow DOM 101 y mi artículo Shadow DOM 201: CSS y diseño.