Modelo, slot e sombra

A vantagem dos componentes da Web é sua reutilização: é possível criar um widget de IU uma vez e reutilizá-lo várias vezes. Enquanto você precisa do JavaScript para criar componentes da Web, uma biblioteca JavaScript não é necessária. O HTML e as APIs associadas oferecem tudo o que você precisa.

O padrão do Web Component é composto por três partes: modelos HTML, Elementos personalizados e o Shadow DOM. Combinados, eles permitem criar elementos personalizados, autossuficientes (encapsulados) reutilizáveis e totalmente integrados em aplicativos existentes, como todos os outros elementos HTML que já abordamos.

Nesta seção, criaremos o elemento <star-rating>, um componente da Web que permite que os usuários avaliem uma experiência em um escala de 1 a 5 estrelas. Ao nomear um elemento personalizado, recomendamos usar somente letras minúsculas. Além disso, inclua um traço, porque isso ajuda a diferenciar elementos regulares e personalizados.

Vamos discutir o uso dos elementos <template> e <slot>, do atributo slot e do JavaScript para criar um modelo com um Shadow DOM encapsulado. Depois, vamos reutilizar o elemento definido, personalizando uma seção de texto, como você faria com qualquer elemento ou componente da Web. Também discutiremos brevemente como usar CSS dentro e fora do elemento personalizado.

O elemento <template>

O elemento <template> é usado para declarar fragmentos de HTML a serem clonados e inseridos no DOM com JavaScript. O conteúdo do elemento não é renderizado por padrão. Em vez disso, eles são instanciados usando JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Como o conteúdo de um elemento <template> não é gravado na tela, o <form> e o conteúdo dele não são renderizados. Sim, o Codepen está em branco, mas se você inspecionar a guia HTML, vai encontrar a marcação <template>.

Nesse exemplo, a <form> não é filha de uma <template> no DOM. Em vez disso, o conteúdo dos elementos <template> são filhos de um DocumentFragment retornado pelo HTMLTemplateElement.content . Para ficar visível, o JavaScript deve ser usado para obter o conteúdo e anexá-lo ao DOM.

Esse breve JavaScript não criou um elemento personalizado. Em vez disso, este exemplo anexou o conteúdo de <template> ao <body>. O conteúdo se tornou parte do DOM visível e estilizado.

Captura de tela do codepen anterior, conforme mostrado no DOM.

Exigir que o JavaScript implemente um modelo com apenas uma avaliação com estrelas não é muito útil, mas criar um componente da Web para um usado repetidamente, o widget personalizável de avaliação com estrelas é útil.

O elemento <slot>

Incluímos um slot para incluir uma legenda personalizada por ocorrência. O HTML fornece um objeto <slot> como um marcador de posição dentro de uma <template> que, se fornecido, vai criar um "slot nomeado". É possível usar um slot nomeado para personalizar o conteúdo em um componente da Web. O elemento <slot> oferece uma maneira de controlar onde os filhos de um deve ser inserido em sua árvore paralela.

No nosso modelo, mudamos <legend> para <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

O atributo name será usado para atribuir slots a outros elementos se ele tiver um atributo slot com valor correspondente ao nome de um slot nomeado. Se o elemento personalizado não tiver uma correspondência para um slot, o conteúdo do <slot> será renderizado. Por isso, incluímos um <legend> com conteúdo genérico que pode ser renderizado se alguém simplesmente incluir <star-rating></star-rating>, sem conteúdo, no HTML.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

O slot é um atributo global usado para substituir o conteúdo de <slot> em uma <template>. Em nosso elemento personalizado, o elemento com o atributo slot é um <legend>. Não precisa ser assim. Em nosso modelo, <slot name="star-rating-legend"> será substituído por <anyElement slot="star-rating-legend">, em que <anyElement> pode ser qualquer elemento, até mesmo outro elemento personalizado.

Elementos indefinidos

No <template>, usamos um elemento <rating>. Este não é um elemento personalizado. Em vez disso, é um elemento desconhecido. Navegadores não falhem quando não reconhecerem um elemento. Elementos HTML não reconhecidos são tratados pelo navegador como anônimos inline elementos que podem ser estilizados com CSS. Assim como <span>, os elementos <rating> e <star-rating> não têm user agent aplicado. estilos ou semânticas.

O <template> e o conteúdo não são renderizados. O <template> é um elemento conhecido que inclui conteúdo que não será renderizado. O elemento <star-rating> ainda não foi definido. Até definirmos um elemento, o navegador o exibe como todos os elementos não reconhecidos. Por enquanto, o <star-rating> não reconhecido é tratado como um elemento inline anônimo, de modo que o conteúdo incluindo as legendas e a <p> na terceira <star-rating>, são exibidas como seriam se estivessem em uma <span>.

Vamos definir nosso elemento para converter esse elemento não reconhecido em um personalizado.

Elementos personalizados

O JavaScript é necessário para definir elementos personalizados. Quando definido, o conteúdo do elemento <star-rating> será substituído por um raiz paralela que contém todo o conteúdo do modelo associado a ele. Os elementos <slot> do modelo são substituídos pelo conteúdo do elemento dentro da <star-rating>, em que o valor do atributo slot corresponde ao valor do nome do <slot>, se existe um. Caso contrário, o conteúdo dos espaços do modelo será exibido.

O conteúdo em um elemento personalizado que não esteja associado a um slot, o <p>Is this text visible?</p> no nosso terceiro <star-rating>, não está incluído no a raiz paralela e, portanto, não são exibidos.

Definimos o elemento personalizado chamado star-rating estendendo o HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Agora que o elemento está definido, sempre que o navegador encontrar um elemento <star-rating>, ele será renderizado conforme definido. pelo elemento #star-rating-template, que é o nosso modelo. O navegador anexará uma árvore do shadow DOM ao nó, anexando um clone do conteúdo do modelo para esse shadow DOM. Os elementos em que você pode attachShadow() são limitados.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Nas ferramentas para desenvolvedores, você vai notar que o <form> do <template> faz parte da raiz paralela de cada elemento personalizado. Um clone do conteúdo de <template> é aparente em cada elemento personalizado nas ferramentas para desenvolvedores e visível no navegador, mas o conteúdo do elemento personalizado não são renderizados na tela.

Captura de tela do DevTools mostrando o conteúdo do modelo clonado em cada elemento personalizado.

No exemplo <template>, anexamos o conteúdo do modelo ao corpo do documento, adicionando o conteúdo ao DOM normal. Na definição de customElements (link em inglês), usamos a mesma appendChild(), mas o conteúdo do modelo clonado foi anexado a uma shadow DOM encapsulado.

Viu como as estrelas voltaram a ser botões de opção sem estilo? Como parte de um shadow DOM e não do DOM padrão, o estilo na guia CSS do Codepen não se aplica. O CSS dessa guia O escopo dos estilos é o documento, não o shadow DOM. Portanto, os estilos não são aplicados. Precisamos criar modelos para estilizar o conteúdo encapsulado do Shadow DOM.

Shadow DOM

O Shadow DOM define o escopo dos estilos CSS para cada árvore paralela, isolando-a do resto do documento. Significa que CSS externo não se aplica ao seu componente, e os estilos de componentes não afetam o restante do documento, a menos que intencionalmente para onde os direcionar.

Como anexamos o conteúdo a um shadow DOM, podemos incluir um elemento <style> fornecer CSS encapsulado ao elemento personalizado.

Como o elemento personalizado está definido como escopo, não precisamos nos preocupar com estilos que se escoam para o restante do documento. Podemos reduzir substancialmente a especificidade dos seletores. Por exemplo, como as únicas entradas usadas no elemento personalizado são opções podemos usar input em vez de input[type="radio"] como um seletor.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Embora os componentes da Web sejam encapsulados com marcação em <template>, e os estilos CSS tenham o escopo definido para o shadow DOM e fiquem ocultos de tudo fora dos componentes, do conteúdo do slot que é renderizado, do <anyElement slot="star-rating-legend"> do <star-rating>, não é encapsulado.

Estilo fora do escopo atual

É possível, mas não simples, estilizar o documento dentro de um shadow DOM e o conteúdo de um shadow DOM usando os estilos globais. O limite da sombra, em que o shadow DOM termina e o DOM regular começa, pode ser percorrido, mas somente intencionalmente.

A árvore de sombra é a árvore do DOM dentro do shadow DOM. A raiz paralela é o nó raiz da árvore paralela.

A pseudoclasse :host seleciona <star-rating>, o elemento host sombra. O host sombra é o nó DOM ao qual o shadow DOM está anexado. Para segmentar apenas versões específicas do host, use :host(). Isso vai selecionar apenas os elementos do host sombra que correspondem ao parâmetro passado, como um seletor de classe ou atributo. Selecionar todos os elementos personalizados, use star-rating { /* styles */ } no CSS global ou :host(:not(#nonExistantId)) nos estilos de modelo. Em termos de especificidade, o CSS global vence.

O pseudoelemento ::slotted() cruza o limite do shadow DOM dentro do shadow DOM. Seleciona um elemento com slots se ele corresponder ao seletor. Em nosso exemplo, ::slotted(legend) corresponde às nossas três legendas.

Para segmentar um shadow DOM a partir do CSS no escopo global, o modelo precisa ser editado. O part pode ser adicionado a qualquer elemento que você queira estilizar. Em seguida, use o pseudoelemento ::part(). para combinar os elementos em uma árvore paralela que correspondem ao parâmetro passado. A âncora ou o elemento de origem do pseudoelemento é o nome do host ou do elemento personalizado, neste caso, star-rating. O parâmetro é o valor do atributo part.

Se a marcação do nosso modelo começasse assim:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Poderíamos segmentar <form> e <fieldset> com:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Os nomes das partes agem de forma semelhante às classes: um elemento pode ter vários nomes de partes separados por espaços, e vários elementos podem têm o mesmo nome de peça.

O Google tem uma lista de verificação fantástica para criar elementos personalizados. Talvez você também queira aprender sobre shadow DOMs declarativos.

Teste seu conhecimento

Teste seus conhecimentos sobre modelo, slot e sombra.

Por padrão, os estilos de fora do shadow DOM definem o estilo dos elementos internos.

Verdadeiro
Tente novamente.
Falso
Correto!

Qual resposta é uma descrição correta do elemento <template>?

Um elemento genérico usado para exibir qualquer conteúdo na sua página.
Tente novamente.
Um elemento de marcador.
Tente novamente.
Um elemento usado para declarar fragmentos de HTML que não serão renderizados por padrão.
Correto!