Introdução
Os componentes da Web são um conjunto de padrões de ponta que:
- Possibilitar a criação de widgets
- …que podem ser reutilizados de forma confiável
- …e que não vão quebrar as páginas se a próxima versão do componente mudar os detalhes de implementação interna.
Isso significa que você precisa decidir quando usar HTML/JavaScript e quando usar componentes da Web? Não! HTML e JavaScript podem criar elementos visuais interativos. Widgets são elementos visuais interativos. Faz sentido aproveitar suas habilidades em HTML e JavaScript ao desenvolver um widget. Os padrões de componentes da Web foram criados para ajudar você a fazer isso.
Mas há um problema fundamental que dificulta o uso de widgets criados com HTML e JavaScript: a árvore do DOM dentro de um widget não é encapsulada 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, os IDs podem se sobrepor aos IDs dentro do widget e assim por diante.
Os componentes da Web são compostos por três partes:
O Shadow DOM resolve o problema de encapsulamento da árvore do DOM. As quatro partes dos componentes da Web foram projetadas para funcionar juntas, mas você também pode escolher quais partes usar. Neste tutorial, mostramos como usar o Shadow DOM.
Olá, Mundo das Sombras
Com o Shadow DOM, os elementos podem receber um novo tipo de nó associado a eles. Esse novo tipo de nó é chamado de raiz de sombra. Um elemento que tem uma raiz de sombra associada a ele é chamado de host de sombra. O conteúdo de um host de sombra não é renderizado. Em vez disso, o conteúdo da raiz de sombra é renderizado.
Por exemplo, se você tiver uma marcação como esta:
<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>
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>
sua página
<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 do botão, ele não vai receber "こんにちは、影の世界!", mas sim "Hello, world!", porque a subárvore do DOM na raiz de sombra está encapsulada.
Separar o conteúdo da apresentação
Agora vamos ver como usar o Shadow DOM para separar o conteúdo da 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>
Veja a marcação. É o que você escreveria hoje. Ele não usa 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 tem encapsulamento, toda a estrutura da tag de nome é exposta ao documento. Se outros elementos na página usarem acidentalmente os mesmos nomes de classe para estilo ou script, vamos ter problemas.
Podemos evitar uma situação ruim.
Etapa 1: ocultar detalhes da apresentação
Semanticamente, provavelmente só nos importamos com o seguinte:
- É um tag de nome.
- O nome é "Bob".
Primeiro, escrevemos uma marcação mais próxima da semântica real 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
movemos os elementos DOM de apresentação para dentro
de um elemento <template>, eles não são renderizados, mas
podem ser acessados pelo JavaScript. Vamos fazer isso agora para
preencher a raiz de sombra:
<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 de sombra, a tag de nome é renderizada novamente. Se você clicar com o botão direito do mouse na tag de nome e inspecionar o elemento, verá que é uma marcação semântica:
<div id="nameTag">Bob</div>
Isso demonstra que, ao usar o Shadow DOM, ocultamos os detalhes de apresentação da tag de nome do documento. 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 os detalhes de apresentação da página, mas não separa a apresentação do conteúdo, porque, embora o conteúdo (o nome "Bob") esteja na página, o nome renderizado é o que copiamos para a raiz de sombra. Se quisermos mudar o nome no tag de nome, precisaremos fazer isso em dois lugares, e eles podem ficar dessincronizados.
Os elementos HTML são composicionais. Por exemplo, é possível colocar um botão dentro de uma tabela. A composição é o que precisamos aqui: o tag de nome precisa ser uma composição do plano de fundo vermelho, do texto "Hi!" e do conteúdo que está no tag de nome.
Você, o 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
ponto de inserção seleciona o conteúdo do host de sombra para apresentar
nesse ponto.
Se mudarmos a marcação no Shadow DOM para:
<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 de sombra é
projetado no local em que o elemento <content>
aparece.
Agora a estrutura do documento é mais simples porque o nome está apenas em um lugar: o documento. Se a página precisar atualizar o nome 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 da
tag de nome no lugar com <content>.
<div id="ex2b">
Agora, separamos o conteúdo da apresentação. O conteúdo está no documento, e a apresentação está no Shadow DOM. Eles são mantidos em sincronia automaticamente pelo navegador quando chega a hora de renderizar algo.
Etapa 3: lucro
Ao separar o conteúdo e a apresentação, podemos simplificar o código que manipula o conteúdo. No exemplo do tag de nome, esse 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 precisaremos mudar nada do código.
Por exemplo, digamos que queremos localizar nossa tag de nome. Ainda é uma tag de nome, então o conteúdo semântico no documento não muda:
<div id="nameTag">Bob</div>
O código de configuração da raiz de sombra permanece o mesmo. Apenas o que é colocado na raiz de sombra muda:
<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 o código de atualização de nome pode depender da estrutura do componente, que é simples e consistente. O código de atualização de nome não precisa conhecer a estrutura usada para renderização. Se considerarmos o que é renderizado, o nome aparece em segundo lugar em inglês (depois de "Hi! My name is"), mas primeiro em japonês (antes de "と申します"). Essa distinção não tem significado semântico do ponto de vista da atualização do nome que está sendo exibido. Portanto, o código de atualização do nome não precisa saber desse detalhe.
Atividade complementar: projeção avançada
No exemplo acima, o elemento <content>
seleciona todo o conteúdo do host de sombra. Ao usar o
atributo select, é possível controlar o que
um elemento de conteúdo projeta. Também é possível usar vários elementos de
conteúdo.
Por exemplo, se você tiver um documento que contenha o seguinte:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
e uma raiz de sombra 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 pelos elementos <content select="div"> e <content
select=".email">. Quantas vezes o endereço de e-mail do Bob aparece e em quais cores?
A resposta é que o endereço de e-mail do Bob aparece uma vez, em amarelo.
O motivo é que, como as pessoas que hackeiam o Shadow DOM sabem,
construir a árvore do que é realmente renderizado na tela é como uma
festa enorme. O elemento de conteúdo é o convite que permite que o conteúdo do documento entre na renderização do Shadow DOM nos bastidores. Esses convites são entregues em ordem. Quem recebe um
convite depende de para quem ele é endereçado (ou seja,
o atributo select). O conteúdo, uma vez
convidado, sempre aceita o convite (quem não aceitaria?!) e
vai embora. Se um convite subsequente for enviado para esse endereço novamente,
ninguém estará em casa e ele não vai chegar à sua festa.
No exemplo acima, <div class="email"> corresponde
ao seletor div e ao seletor
.email, mas como o elemento de conteúdo com o seletor
div aparece antes no documento,
<div class="email"> vai para a festa amarela, e
ninguém está disponível para ir à festa azul. (Talvez seja por isso que ele é tão azul, embora a tristeza goste de companhia, então nunca se sabe.)
Se algo não for convidado para nenhuma festa, ele não será renderizado. Foi o que aconteceu com o texto "Hello, world" no primeiro exemplo. Isso é útil quando você quer uma renderização radicalmente diferente: escreva o modelo semântico no documento, que é o que pode ser acessado por scripts na página, mas oculte-o para fins de renderização e conecte-o a um modelo de renderização realmente diferente no Shadow DOM usando JavaScript.
Por exemplo, o HTML tem um bom seletor de data. Se você escrever <input
type="date">, vai receber um calendário pop-up bem organizado. Mas e se você
quiser permitir que o usuário escolha um período para as férias em uma ilha de sobremesas
(com redes feitas de alcaçuz, por exemplo)? Configure
o documento desta 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 destaque o intervalo de datas e assim por diante. Quando o usuário clica nos dias no calendário, o componente atualiza o estado nas entradas startDate e endDate. Quando o usuário envia o formulário, os valores desses elementos de entrada são enviados.
Por que incluí rótulos no documento se eles não serão renderizados? O motivo é que, se um usuário visualizar o formulário com um navegador que não é compatível com o Shadow DOM, o formulário ainda poderá ser usado, mas não será tão bonito. O usuário verá 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>
Você passou no curso básico de Shadow DOM
Essas são as noções básicas do Shadow DOM. Você passou no curso básico! É possível fazer mais com o Shadow DOM. Por exemplo, você pode usar vários shadows em um host shadow, shadows aninhados para encapsulamento ou arquitetar sua página usando visualizações orientadas por modelo (MDV, na sigla em inglês) e o Shadow DOM. E os componentes da Web são mais do que apenas o Shadow DOM.
Vamos explicar isso em postagens futuras.