Introdução ao Shadow DOM

Dominic Cooney
Dominic Cooney

Introdução

O Web Components é um conjunto de padrões modernos que:

  1. Possibilitar a criação de widgets
  2. ... que podem ser reutilizados de maneira confiável
  3. E que não corromper as páginas se a próxima versão do componente altera os detalhes de implementação internos.

Isso significa que você precisa decidir quando usar HTML/JavaScript e quando usar os Web Components? Não! HTML e JavaScript podem tornar elementos visuais interativos. Os widgets são recursos visuais interativos. Ela faz sentido aproveitar suas habilidades HTML e JavaScript ao desenvolver um widget. Os padrões de componentes da Web são projetados para ajudar fazer isso.

Mas há um problema fundamental que torna os widgets criados a partir HTML e JavaScript difíceis de usar: a árvore do DOM em um widget não é encapsulados do restante da página. Essa falta de encapsulamento significa que a folha de estilo do documento pode ser aplicada acidentalmente a partes dentro do widget. o JavaScript pode modificar acidentalmente partes dentro do widget. seus IDs podem se sobrepor aos IDs dentro do widget. e assim por diante.

O Web Components é composto por três partes:

  1. Modelos
  2. Shadow DOM
  3. Elementos personalizados

O Shadow DOM resolve o problema de encapsulamento da árvore do DOM. A quatro partes dos Web Components são projetadas para funcionar juntas, mas você pode escolher quais partes dos Web Components usar. Isso mostra como usar o Shadow DOM.

Olá, Mundo das Sombras

Com o Shadow DOM, os elementos podem receber um novo tipo de nó associado para resolvê-los com rapidez. Esse novo tipo de nó é chamado de raiz paralela. Um elemento que tem uma raiz paralela associada a ele é chamado de sombra host. O conteúdo de um host sombra não é renderizado. o conteúdo de a raiz paralela será renderizada.

Por exemplo, se você tivesse uma marcação como esta:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

então, em vez de

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

como sua página está

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Além disso, se o JavaScript na página perguntar qual é o textContent não, "ここんろちちん影の世界!", mas "Olá, mundo!" porque a subárvore do DOM na raiz paralela é encapsulado.

Como separar o conteúdo da apresentação

Agora veremos como usar o Shadow DOM para separar o conteúdo do apresentação. Digamos que temos esta tag de nome:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Aqui está a marcação. Isso é o que você escreveria hoje. Ela não use o Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Como a árvore do DOM não possui encapsulamento, toda a estrutura do tag de nome é exposta ao documento. Se outros elementos da página acidentalmente os mesmos nomes de classe para estilo ou script, estamos não vamos nos passar.

Podemos evitar ter um momento ruim.

Etapa 1: ocultar detalhes da apresentação

Semanticamente, provavelmente só importa:

  • É uma tag de nome.
  • O nome é “Bob”.

Primeiro, escrevemos uma marcação mais próxima da verdadeira semântica que queremos:

<div id="nameTag">Bob</div>

Em seguida, colocamos todos os estilos e divs usados para apresentação em um elemento <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

Neste ponto, "Bob" é a única coisa renderizada. Como nós moveu os elementos DOM de apresentação para dentro um elemento <template>, eles não serão renderizados, elas podem ser acessadas em JavaScript. Fazemos isso agora para preencha a raiz paralela:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Agora que configuramos uma raiz paralela, a tag de nome é renderizada de novo. Se você clicasse com o botão direito na tag de nome e inspecionasse você vê que é uma marcação agradável e semântica:

<div id="nameTag">Bob</div>

Isso demonstra que, usando o Shadow DOM, ocultamos os de apresentação da tag de nome do documento. A os detalhes da apresentação são encapsulados no Shadow DOM.

Etapa 2: separar o conteúdo da apresentação

Nossa tag de nome agora oculta da página os detalhes da apresentação, não separa apresentação do conteúdo, porque, o conteúdo (o nome "Bob") está na página, o nome que é renderizado que copiamos para a raiz paralela. Se quisermos mudar na tag de nome, precisaríamos fazer isso em dois lugares, e eles podem saiam de sincronia.

Os elementos HTML são composicionais: você pode colocar um botão dentro de uma tabela, por exemplo. A composição é o que precisamos aqui: a tag de nome deve ser um composição do fundo vermelho, o “Oi!” o texto e o conteúdo que está na tag de nome.

Você, autor do componente, define como a composição funciona com seu widget usando um novo elemento chamado <content>. Isso cria um ponto de inserção na apresentação do widget e o o ponto de inserção seleciona conteúdo do host sombra para apresentar naquele momento.

Se mudarmos a marcação no Shadow DOM para esta:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Quando a tag de nome é renderizada, o conteúdo do host sombra é projetada no local em que o elemento <content> aparece.

Agora, a estrutura do documento está mais simples, porque o nome é apenas em um só lugar: o documento. Se sua página precisar atualizar do usuário, basta escrever:

document.querySelector('#nameTag').textContent = 'Shellie';

e pronto. A renderização da tag de nome é atualizada automaticamente pelo navegador, porque estamos projetando o conteúdo do no lugar com <content>.

<div id="ex2b">

Agora fizemos a separação do conteúdo e da apresentação. O o conteúdo está no documento. a apresentação está no Shadow DOM. Eles são sincronizados automaticamente pelo navegador quando chega a hora para renderizar algo.

Etapa 3: lucro

Ao separar o conteúdo e a apresentação, podemos simplificar a que manipula o conteúdo, no exemplo da tag de nome, que código só precisa lidar com uma estrutura simples que contém um <div> em vez de vários.

Agora, se mudarmos nossa apresentação, não precisamos alterar nenhum dos o código-fonte é alterado.

Por exemplo, digamos que queremos localizar nossa tag de nome. Ainda é um nome tag para que o conteúdo semântico no documento não mude:

<div id="nameTag">Bob</div>

O código de configuração da raiz paralela permanece o mesmo. Apenas o que é colocado mudanças na raiz paralela:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Essa é uma grande melhoria em relação à situação atual na Web, porque seu código de atualização de nome pode depender da estrutura component que é simples e consistente. Seu nome o código de atualização não precisa conhecer a estrutura usada para renderização. Se considerarmos o que é renderizado, o nome aparecerá segundo em inglês (depois de “Hi! Meu nome é"), mas primeiro em japonês (antes de "申ます"). Essa distinção é semanticamente sem sentido do ponto de vista da atualização do nome exibido, para que o código de atualização do nome não precise saber desses detalhes.

Crédito extra: projeção avançada

No exemplo acima, o elemento <content> seleciona a dedo todo o conteúdo do host sombra. Ao usar o método select, é possível controlar quais de um elemento de conteúdo. Você também pode usar vários conteúdos os elementos.

Por exemplo, se você tiver um documento que contenha isto:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

e uma raiz paralela que usa seletores de CSS para selecionar conteúdo específico:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

O elemento <div class="email"> é correspondido por ambos os elementos <content select="div"> e <content select=".email">. Quantas vezes o e-mail de Beto envia e em quais cores?

A resposta é que o endereço de e-mail de Bob aparece uma vez e está amarelo.

O motivo é que, como as pessoas que invadem no Shadow DOM sabem, e construir a árvore do que é realmente renderizado na tela é como uma uma festa enorme. O elemento de conteúdo é o convite que permite conteúdo do documento para a renderização dos bastidores do Shadow DOM parte. Os convites são entregues em ordem: quem recebe do convite depende de para quem ele é dirigido (ou seja, o atributo select. Conteúdo, uma vez convidado, sempre aceita o convite (quem não aceitaria?!) e desativou-o. vai Se um convite subsequente for enviado para esse endereço novamente, Não há ninguém em casa nem ele vai na sua festa.

No exemplo acima, <div class="email"> corresponde a o seletor div e o .email seletor, mas como o elemento de conteúdo com o elemento div é exibido no início do documento, <div class="email"> vai para a festa amarela e ninguém está disponível para comparecer à festa azul. Isso pode por que é tão azul, embora a tristeza ame companhia, nunca se sabe.

Se um item for convidado para não partes, ele não será renderizados. Foi isso que aconteceu com o texto “Hello, world” na no primeiro exemplo. Isso é útil quando você deseja obter renderização radicalmente diferente: escreva o modelo semântico na que é acessível para os scripts na página, mas ocultam para fins de renderização e conectá-lo a uma interface no Shadow DOM usando JavaScript.

Por exemplo, o HTML tem um bom seletor de data. Se você escrever para <input type="date">, vai receber um ótimo calendário pop-up. Mas e se você Permitir que o usuário escolha um período para a sobremesa férias na ilha (você sabe... com redes feitas de Red Vines). Você configure seu documento da seguinte forma:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

mas criar um Shadow DOM que use uma tabela para criar um calendário elegante que destaca o intervalo de datas e assim por diante. Quando o usuário clica os dias no calendário, o componente atualiza o estado no entradas startDate e endDate; quando o usuário enviar o formulário, a valores desses elementos de entrada são enviados.

Por que incluí rótulos no documento se eles não seriam renderizado? Isso porque, se um usuário abrir o formulário em um navegador, que não seja compatível com o Shadow DOM, o formulário ainda poderá ser usado, mas não bonitos. O usuário vê algo como:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Introdução ao Shadow DOM

Esses são os fundamentos do Shadow DOM: você transmite o Shadow DOM 101! Você pode fazer mais com o Shadow DOM, por exemplo, podendo usar múltiplos elementos um host sombra ou sombras aninhadas para encapsulamento ou sua página usando visualizações orientadas a modelos (MDV, na sigla em inglês) e o Shadow DOM. E Web Os componentes são mais do que apenas o Shadow DOM.

Vamos explicar isso em outras postagens.