Shadow DOM 301

Conceitos avançados e APIs DOM

Este artigo aborda mais coisas incríveis que você pode fazer com o Shadow DOM. Ele se baseia nos conceitos discutidos em Shadow DOM 101 e Shadow DOM 201.

Como usar várias raízes de sombra

Se você estiver uma festa, o ambiente vai ficar abafado se todo mundo estiver no mesmo cômodo. Você quer ter a opção de distribuir grupos de pessoas em várias salas. Os elementos que hospedam o Shadow DOM também podem fazer isso, ou seja, podem hospedar mais de uma raiz de sombra por vez.

Vamos ver o que acontece se tentarmos anexar várias raízes de sombra a um 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>

O que é renderizado é "Root 2 FTW", apesar de já termos anexado uma árvore de sombra. Isso ocorre porque a última árvore de sombra adicionada a um host vence. É uma pilha LIFO no que diz respeito à renderização. Ao examinar o DevTools, você confirma esse comportamento.

Então, qual é o sentido de usar várias sombras se apenas a última é convidada para a festa de renderização? Insira os pontos de inserção de sombra.

Pontos de inserção de sombra

Os "pontos de inserção de sombra" (<shadow>) são semelhantes aos pontos de inserção normais (<content>) porque são marcadores de posição. No entanto, em vez de serem marcadores de posição para o conteúdo de um host, eles são hosts para outras árvores de sombra. É o início do Shadow DOM!

Como você deve imaginar, as coisas ficam mais complicadas quanto mais você se aprofunda no assunto. Por esse motivo, a especificação é muito clara sobre o que acontece quando vários elementos <shadow> estão em jogo:

Voltando ao nosso exemplo original, a primeira sombra root1 foi deixada de fora da lista de convites. Adicionar um ponto de inserção <shadow> o traz de volta:

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

Há alguns pontos interessantes sobre esse exemplo:

  1. "Root 2 FTW" ainda é renderizado acima de "Root 1 FTW". Isso ocorre devido ao local em que colocamos o ponto de inserção <shadow>. Se você quiser o inverso, mova o ponto de inserção: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Agora há um ponto de inserção <content> em root1. Isso faz com que o nó de texto "Light DOM" seja incluído na renderização.

O que é renderizado em <shadow>?

Às vezes, é útil saber qual árvore de sombra mais antiga está sendo renderizada em um <shadow>. Você pode receber uma referência a essa árvore por meio de .olderShadowRoot:

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

Como conseguir a raiz de sombra de um host

Se um elemento estiver hospedando o Shadow DOM, você poderá acessar a raiz paralela mais recente usando .shadowRoot:

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

Se você estiver preocupado com pessoas cruzando suas sombras, redefina .shadowRoot como nulo:

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

É um pouco gambiarra, mas funciona. No fim das contas, é importante lembrar que, embora seja incrível, o Shadow DOM não foi projetado para ser um recurso de segurança. Não confie nele para o isolamento completo do conteúdo.

Como criar um DOM paralelo em JS

Se você prefere criar DOM em JS, HTMLContentElement e HTMLShadowElement têm interfaces para isso.

<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 exemplo é quase idêntico ao da seção anterior. A única diferença é que agora estou usando select para extrair o <span> recém-adicionado.

Como trabalhar com pontos de inserção

Os nós selecionados fora do elemento host e "distribuídos" na árvore de sombra são chamados de… rufem os tambores… nós distribuídos. Eles podem cruzar o limite de sombra quando os pontos de inserção os convidam.

O que é conceitualmente bizarro sobre os pontos de inserção é que eles não movem fisicamente o DOM. Os nós do host permanecem intactos. Os pontos de inserção apenas reprojetam nós do host na árvore de sombra. É uma questão de apresentação/renderização: "Mova esses nós para cá" "Renderize esses nós neste local".

Exemplo:

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

Isso é o suficiente para O h2 não é um filho do shadow DOM. Isso nos leva a outra informação:

Element.getDistributedNodes()

Não é possível atravessar um <content>, mas a API .getDistributedNodes() permite consultar os nós distribuídos em um ponto de inserção:

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

Assim como em .getDistributedNodes(), é possível verificar em quais pontos de inserção um nó é distribuído chamando o .getDestinationInsertionPoints() dele:

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

Ferramenta: Shadow DOM Visualizer

Entender a magia negra que é o Shadow DOM é difícil. Lembro de tentar entender isso pela primeira vez.

Para ajudar a visualizar como funciona a renderização do Shadow DOM, criei uma ferramenta usando d3.js. As duas caixas de marcação à esquerda são editáveis. Fique à vontade para colar sua própria marcação e testar para ver como as coisas funcionam e como os pontos de inserção misturam os nós de host na árvore de sombra.

Visualizador de DOM de sombra
Iniciar o Shadow DOM Visualizer

Faça um teste e conte o que achou!

Modelo de evento

Alguns eventos cruzam o limite de sombra e outros não. Nos casos em que os eventos atravessam o limite, o destino do evento é ajustado para manter o encapsulamento fornecido pelo limite superior da raiz de sombra. Ou seja, os eventos são redirecionados para parecer que vieram do elemento host, e não de elementos internos do Shadow DOM.

Ação 1 do Google Play

  • Este é interessante. Você verá um mouseout do elemento host (<div data-host>) para o nó azul. Mesmo que seja um nó distribuído, ele ainda está no host, não no ShadowDOM. Mover o mouse mais para baixo no amarelo novamente causa um mouseout no nó azul.

Ação do Google Play 2

  • Há um mouseout que aparece no host (no final). Normalmente, você veria eventos mouseout acionados para todos os blocos amarelos. No entanto, nesse caso, esses elementos são internos ao Shadow DOM, e o evento não se propaga pelo limite superior.

Ação do Google Play 3

  • Ao clicar na entrada, o focusin não aparece nela, mas no próprio nó do host. Ele foi redirecionado!

Eventos que estão sempre interrompidos

Os seguintes eventos nunca cruzam o limite de sombra:

  • cancel
  • erro
  • selecionar
  • alterar
  • pilha
  • redefinir
  • resize
  • scroll
  • selectstart

Conclusão

Espero que você concorde que o DOM paralelo é incrivelmente poderoso. Pela primeira vez, temos um encapsulamento adequado sem a bagagem extra de <iframe>s ou outras técnicas mais antigas.

O Shadow DOM é certamente uma fera complexa, mas é uma fera que vale a pena adicionar à plataforma da Web. Dedique um tempo a ele. Aprenda Faça perguntas.

Para saber mais, consulte o artigo de introdução do Dominic, Shadow DOM 101, e meu artigo Shadow DOM 201: CSS e estilo.