Conceitos avançados e APIs do DOM
Neste artigo, discutimos mais sobre as 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ê está dando uma festa, o ambiente fica abafado se todo mundo estiver amontoado no mesmo lugar. e quer distribuir os 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 paralela por vez.
Vamos conferir o que acontece se tentarmos anexar várias raízes 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 renderiza é "Root 2 FTW", apesar de já termos adicionado uma árvore paralela. Isso acontece porque a última árvore de sombra adicionada a um host vence. É uma pilha LIFO no que se refere à renderização. Analisar as DevTools verifica esse comportamento.
Então, qual é o objetivo de usar várias sombras se apenas a última é convidada para a renderização? Insira 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 (<content>
) normais, 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 Shadow DOM Inception!
Como você provavelmente pode imaginar, as coisas se tornam mais complicadas à medida que você explora a toca do coelho. Por esse motivo, a especificação é muito clara sobre o que acontece quando
vários elementos <shadow>
estão em jogo:
Voltando ao 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á algumas coisas interessantes sobre esse exemplo:
- "Root 2 FTW" ainda é renderizado acima de "Root 1 FTW". Isso ocorre porque 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>';
. - Agora há um ponto de inserção
<content>
em root1. Isso faz com que o nó de texto "Light DOM" apareça na renderização.
O que é renderizado em <shadow>
?
Às vezes, é útil saber qual é a árvore de sombra mais antiga renderizada em um <shadow>
. Você pode conseguir uma referência a essa árvore usando .olderShadowRoot
:
**root2.olderShadowRoot** === root1 //true
Como conseguir a raiz paralela 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 que cruzam suas sombras, redefina
.shadowRoot
como nulo:
Object.defineProperty(host, 'shadowRoot', {
get: function() { return null; },
set: function(value) { }
});
É um pouco de trapaça, mas funciona. No final, é importante lembrar que, embora incrivelmente fantástico, o Shadow DOM não foi projetado para ser um recurso de segurança. Não confie nele para isolar completamente o conteúdo.
Como criar um shadow DOM em JS
Se você preferir 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>
Esse exemplo é quase idêntico ao da seção anterior.
A única diferença é que agora estou usando select
para extrair o <span>
adicionado recentemente.
Como trabalhar com pontos de inserção
Os nós que são selecionados fora do elemento host e "distribuídos" para a árvore de sombra são chamados de…🥁🥁🥁nós distribuídos! Eles podem cruzar a fronteira da 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 projetam novamente os nós
do host na árvore de sombra. Isso é algo de apresentação/renderização: "Mova esses nós para cá" "Renderizar estes 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 é filho do shadow DOM. Isso leva a outro detalhe:
Element.getDistributedNodes()
Não é possível transferir para uma <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()
Semelhante a .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 do 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 o d3.js. As duas caixas de marcação do lado esquerdo são editáveis. Cole sua própria marcação e confira como as coisas funcionam e os pontos de inserção fazem o swizzling de nós do host na árvore paralela.
Faça um teste e conte o que achou.
Modelo de evento
Alguns eventos cruzam a fronteira da sombra e outros não. Nos casos em que os eventos ultrapassam o limite, o destino do evento é ajustado para manter o encapsulamento oferecido pelo limite superior da raiz paralela. 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 de reprodução 1
- Essa é interessante. Você vai encontrar um
mouseout
do elemento host (<div data-host>
) para o nó azul. Embora seja um nó distribuído, ele ainda está no host, não no ShadowDOM. Mover o mouse para baixo em amarelo novamente faz com que ummouseout
seja exibido no nó azul.
Ação de jogar 2
- Há um
mouseout
que aparece no host (no final). Normalmente, você veria eventosmouseout
acionados para todos os blocos amarelos. No entanto, neste caso, esses elementos são internos ao shadow DOM, e o evento não é transmitido pelo limite superior.
Ação de jogar 3
- Quando você clica na entrada, o
focusin
não aparece na entrada, mas no próprio nó host. O público-alvo foi alterado.
Eventos que são sempre interrompidos
Os eventos a seguir nunca cruzam a fronteira da sombra:
- cancel
- erro
- select
- alterar
- autoinfligida
- redefinir
- resize
- scroll
- selectstart
Conclusão
Esperamos que você concorde que o Shadow DOM é muito poderoso. Pela primeira vez, oferecemos um encapsulamento adequado sem a bagagem extra de <iframe>
s ou outras técnicas mais antigas.
O Shadow DOM é certamente muito complexo, mas vale a pena adicioná-lo à plataforma da Web. Invista algum tempo nele. Aprenda. Faça perguntas.
Para saber mais, consulte o artigo de introdução Shadow DOM 101 de Dominic e o artigo Shadow DOM 201: CSS e estilo (links em inglês).