Shadow DOM 301

Conceptos avanzados y APIs de DOM

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

Cómo usar varias shadow roots

Si estás organizando una fiesta, el ambiente se pone sofocante si todos están en la misma habitación. Quieres la opción de distribuir grupos de personas en varias habitaciones. Los elementos que alojan Shadow DOM también pueden realizar esta tarea, es decir, pueden alojar más de una shadow root a la vez.

Veamos qué sucede si intentamos adjuntar varias shadow roots 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 shadow tree. Esto se debe a que gana el último shadow tree agregado a un host. Se trata de una pila LIFO. En lo que respecta a la renderización, Si examinas las Herramientas para desarrolladores, se verificará este comportamiento.

Entonces, ¿para qué sirve usar varias sombras si solo se invita a la última a la parte de renderización? Ingresa 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, ya que son marcadores de posición. Sin embargo, en lugar de ser marcadores de posición para el contenido de un host, son hosts de otros shadow tree. Es el origen de Shadow DOM.

Como probablemente te imagines, las cosas se complican cuanto más ahondas en la madriguera. Por este motivo, la especificación es muy clara sobre lo que sucede cuando hay varios elementos <shadow> en juego:

Si volvemos al ejemplo original, la primera sombra root1 dejó de estar en la lista de invitados. Agregar un punto de inserción <shadow> lo devuelve:

<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 acerca de este ejemplo:

  1. "Root 2 FTW" aún se renderiza por encima de "Root 1 FTW". Esto se debe a que colocamos el punto de inserción <shadow>. Si quieres la opción inversa, 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. De esta manera, se incluye el nodo de texto "Light DOM" en el recorrido de renderización.

¿Qué se renderiza en <shadow>?

A veces, es útil conocer el shadow tree 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 shadow root 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 en tus sombras, redefine .shadowRoot para que sea nulo:

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

Es un truco, pero funciona. Al final, 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 el aislamiento de contenido completo.

Cómo compilar Shadow DOM en JS

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

<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 estoy usando select para extraer el elemento <span> que se agregó recientemente

Cómo trabajar con puntos de inserción

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

Un concepto extraño sobre los puntos de inserción es que no mueven físicamente el DOM. Los nodos del host permanecen intactos. Los puntos de inserción simplemente reproyectan los nodos del host en el shadow tree. Es un aspecto de la presentación o la 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 shadow DOM. Esto nos lleva a otro tid bit:

Element.getDistributedNodes()

No se puede desviar a <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()

De manera similar a .getDistributedNodes(), puedes verificar en qué puntos de inserción se distribuye un nodo mediante una llamada 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: Shadow DOM Visualizer

Es difícil comprender la magia negra que es Shadow DOM. Recuerdo haber tratado de tratar de entenderlo por primera vez.

Compilamos una herramienta con d3.js para ayudar a visualizar cómo funciona la renderización de Shadow DOM. Se pueden editar los dos cuadros de marcado de la izquierda. Si lo deseas, puedes pegar tu propio lenguaje de marcado y jugar para ver cómo funcionan los elementos y los puntos de inserción guían los nodos del host al shadow tree.

Visualizador de Shadow DOM
Iniciar Shadow DOM Visualizer

Pruébalo y dame tu opinión.

Modelo del evento

Algunos eventos cruzan la frontera de la sombra y otros no. Cuando 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 shadow root. Es decir, los eventos se vuelven a segmentar para que parezca que provienen del elemento host, en lugar de elementos internos del Shadow DOM.

Acción de Play 1

  • Este es interesante. Deberías ver un mouseout desde el elemento de host (<div data-host>) hasta el nodo azul. Aunque sea un nodo distribuido, aún está en el host, no en el ShadowDOM. Volver a mover el mouse hacia abajo en el amarillo provoca una mouseout en el nodo azul.

Acción de Play 2

  • Aparece un mouseout en el host (al final). Por lo general, se activarían eventos mouseout para todos los bloques amarillos. Sin embargo, en este caso, estos elementos son internos del Shadow DOM y el evento no pasa por el límite superior.

Acción de Play 3

  • Ten en cuenta que, cuando haces clic en la entrada, focusin no aparece en ella, sino en el nodo host. ¡Se volvió a atacar!

Eventos que siempre se detienen

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

  • abort
  • error
  • select
  • cambiar
  • autoinflingidas
  • reset
  • resize
  • scroll
  • seleccionar iniciar

Conclusión

Espero que estés de acuerdo en que Shadow DOM es increíblemente potente. Por primera vez, tenemos un encapsulamiento adecuado sin el equipaje adicional de <iframe>s ni otras técnicas más antiguas.

Ciertamente, Shadow DOM es un monstruo complejo, pero vale la pena agregarlo a la plataforma web. Dedícale tiempo. Aprender. Haz preguntas.

Si deseas 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.