Shadow DOM, modelos e slots

Recursos da Web: Shadow DOM <slot>

Uma raiz de sombra é uma subárvore DOM encapsulada anexada a um elemento host: os estilos dentro dela não vazam, e os estilos de página não vazam (exceto as propriedades herdadas). Os modelos constroem a árvore de forma eficiente, e os slots projetam o conteúdo do Light DOM nela.

Como anexar uma raiz paralela

this.attachShadow({ mode: 'open', delegatesFocus: true });

delegatesFocus: true faz com que o foco no host seja movido para o primeiro elemento focalizável dentro da árvore de sombra, encaminha o estilo :focus para o host e, em elementos personalizados associados a formulários (static formAssociated = true), encaminha cliques em <label> para o controle interno. Defina-o sempre que o componente envolver controles focalizáveis. Use mode: 'open' em quase todos os casos para que as ferramentas para desenvolvedores, os scripts de página e a hidratação do servidor possam acessar element.shadowRoot (consulte Orientação sobre componentes da Web).

Modelos: construa a árvore de forma eficiente

Defina um <template> uma vez (escopo do módulo ou um campo estático) e clone-o por instância. Clonar um fragmento analisado é muito mais rápido do que analisar novamente uma string innerHTML em cada construção.

const template = document.createElement('template');
template.innerHTML = `
  <div class="card">
    <slot name="header"></slot>
    <div class="body"><slot></slot></div>
    <slot name="footer"></slot>
  </div>
`;

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // MANDATORY: deep-clone the template content so each instance gets its own nodes.
    this.shadowRoot.append(template.content.cloneNode(true));
  }
}
customElements.define('my-card', MyCard);

Mantenha os modelos simples. A "sopa de divs" profundamente aninhada e muitos níveis de raízes de sombra aninhadas adicionam um custo mensurável de resolução de estilo e layout, um grande impacto no desempenho em escala. Prefira uma estrutura simples e elementos semânticos.

Slots: as partes que surpreendem as pessoas

Os slots padrão e nomeados funcionam conforme documentado. Estes são os comportamentos que podem causar problemas:

  • Os espaços em branco contam. Os nós de texto entre elementos (incluindo espaços em branco puros) são atribuídos ao slot padrão, então um slot padrão raramente está realmente "vazio".
  • A ordem do DOM é preservada, não a ordem dos slots. Os nós projetados mantêm a ordem original do Light DOM para fins de :first-child/:last-child, independentemente do slot que preenchem. Uma regra ::slotted(*:first-child) segmenta o primeiro filho do DOM, não o primeiro filho desse slot. Por isso, muitas vezes ela não corresponde ao que você espera.
  • slotchange é o indicador de tempo de upgrade. Ele é acionado quando os nós atribuídos mudam (incluindo a atribuição inicial) e é a maneira confiável de saber que os filhos do Light DOM chegaram após um upgrade, quando talvez não existissem durante connectedCallback.
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

Como carregar folhas de estilo em uma raiz de sombra

Método Prós Contras
<style> no modelo Simples, totalmente encapsulado, funciona sem JS. Duplicado por instância, o que aumenta o DOM em escala.
<link rel="stylesheet"> Arquivo externo que pode ser armazenado em cache. Solicitação extra; corre o risco de um flash de conteúdo sem estilo.
adoptedStyleSheets Um CSSStyleSheet compartilhado em todas as instâncias; mais rápido em escala. Requer JS. As páginas construídas não são compatíveis com @import.

Para muitas instâncias de um componente, prefira uma folha de estilo construída compartilhada:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // The same sheet object is reused by every instance; no per-instance parse.
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}