Shadow DOM v1: componentes da Web independentes

O Shadow DOM permite que os desenvolvedores Web criem DOM e CSS compartimentalizados para componentes da Web

Resumo

O Shadow DOM remove as complicações da criação de apps da Web. A fragilidade vem da natureza global do HTML, CSS e JS. Ao longo dos anos, inventaram um número exorbitante de ferramentas para contornar esses problemas. Por exemplo, quando você usa um novo ID/classe HTML, não é possível saber se ele entrará em conflito com um nome existente usado pela página. Insetos sutis surgem, A especificidade do CSS torna-se um grande problema (!important tudo!), o estilo seletores crescem fora de controle e o desempenho pode ser afetado. A lista continua.

O Shadow DOM corrige o CSS e o DOM. Ele introduz estilos com escopo na Web. de plataforma. Sem ferramentas ou convenções de nomenclatura, você pode empacotar CSS com marcação de usuários, ocultar detalhes de implementação e criar nomes de código aberto em JavaScript básico.

Introdução

O Shadow DOM é um dos três padrões de componentes da Web: Modelos HTML, Shadow DOM e Elementos personalizados. Importações HTML costumava fazer parte da lista, mas agora são considerados suspenso.

Não é necessário criar componentes da Web que usam o shadow DOM. Mas, quando você faz isso, aproveite seus benefícios (escopo de CSS, encapsulamento de DOM, composição) e criar modelos reutilizáveis elementos personalizados, resilientes, altamente configuráveis e reutilizáveis. Se for personalizado são a forma de criar um novo HTML (com uma API JS), o shadow DOM é maneira de fornecer seu HTML e CSS. As duas APIs se combinam para fazer um componente com HTML, CSS e JavaScript independentes.

O Shadow DOM é projetado como uma ferramenta para a criação de apps baseados em componentes. Portanto, ele traz soluções para problemas comuns no desenvolvimento da Web:

  • DOM isolado: o DOM de um componente é independente (por exemplo, document.querySelector() não vai retornar nós no shadow DOM do componente.
  • CSS com escopo: o CSS definido no shadow DOM tem escopo para ele. Regras de estilo não vazem e os estilos de página não interferem.
  • Composição: crie uma API declarativa e baseada em marcação para o componente.
  • Simplificar o CSS: o DOM com escopo significa que você pode usar seletores de CSS simples e muito mais. nomes genéricos de ID/classe e não se preocupe com conflitos de nomes.
  • Produtividade: pense nos apps como blocos de DOM em vez de um único (global).
.

Demonstração de fancy-tabs

Neste artigo, faremos referência a um componente de demonstração (<fancy-tabs>) e fazer referência a snippets de código. Se o navegador for compatível com as APIs, você verá uma demonstração dele logo abaixo. Caso contrário, confira a fonte completa no GitHub (link em inglês).

Ver código-fonte no GitHub

O que é o shadow DOM?

Contexto sobre o DOM

O HTML impulsiona a Web porque é fácil de trabalhar. Ao declarar algumas tags, você consegue criar uma página em segundos com apresentação e estrutura. No entanto, sozinho, o HTML não é tão útil. É fácil para os humanos entenderem um texto, baseada em linguagem natural, mas as máquinas precisam de algo mais. Insira o objeto Document Model, ou DOM.

Quando o navegador carrega uma página da Web, ele faz muitas coisas interessantes. Um de ela transforma o HTML do autor em um documento dinâmico. Basicamente, para entender a estrutura da página, o navegador analisa HTML (estático, strings de texto) em um modelo de dados (objetos/nós). O navegador preserva a hierarquia de HTML criando uma árvore desses nós: o DOM. O legal sobre o DOM é que ele é uma representação ativa da página. Ao contrário da imagem estática o HTML criado por nós, os nós gerados pelo navegador contêm propriedades, métodos e melhores podem ser manipuladas por programas! É por isso que criamos o DOM elementos diretamente usando JavaScript:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

produz a seguinte marcação HTML:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Tudo isso é muito bom. Depois, o que é o shadow DOM?

DOM... à sombra

O Shadow DOM é apenas o DOM normal, com duas diferenças: 1) a forma como é criado/usado 2) como ele se comporta em relação ao restante da página. Normalmente, você cria o DOM nós e os anexa como filhos de outro elemento. Com o shadow DOM, você criar uma árvore do DOM com escopo que é anexada ao elemento, mas separada de seu crianças reais. Essa subárvore com escopo é chamada de árvore paralela. O elemento ao qual está anexada é o host sombra. Tudo que você adiciona em sombra se torna local ao elemento de hospedagem, incluindo <style>. É assim que o shadow DOM atinge o escopo de estilo do CSS.

Como criar o shadow DOM

Uma raiz paralela é um fragmento de documento anexado a um elemento "host". O elemento obtém seu shadow DOM mediante a anexação de uma raiz paralela. Para criar um shadow DOM para um elemento, chame element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Estou usando .innerHTML para preencher a raiz paralela, mas você também pode usar outros elementos DOM. APIs de terceiros. Esta é a Web. Temos escolha.

A especificação define uma lista de elementos. que não pode hospedar uma árvore paralela. Há vários motivos para um elemento ser na lista:

  • O navegador já hospeda o próprio shadow DOM para o elemento (<textarea>, <input>).
  • Não faz sentido que o elemento hospede um shadow DOM (<img>).

Por exemplo, isto não funciona:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Como criar um shadow DOM para um elemento personalizado

O Shadow DOM é particularmente útil ao criar elementos personalizados. Use o shadow DOM para compartimentar o HTML, o CSS e o JS de um elemento. produzindo um "componente da Web".

Exemplo: um elemento personalizado anexa o shadow DOM a si mesmo. encapsulando seu DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Algumas coisas interessantes estão acontecendo aqui. A primeira é que um elemento personalizado cria o próprio shadow DOM quando uma instância de <fancy-tabs> é criada. Isso é feito no constructor(). Em segundo lugar, como estamos criando uma raiz paralela, as regras de CSS no <style> terão o escopo definido como <fancy-tabs>.

Composição e slots

A composição é um dos recursos menos compreendidos do shadow DOM, mas é sem dúvida a mais importante.

Em nosso mundo de desenvolvimento da Web, a composição é o modo como construímos aplicativos, de forma declarativa fora do HTML. Elementos básicos diferentes (<div>s, <header>s, <form>s, <input>s) se unem para formar apps. Algumas dessas tags funcionam até e se relacionam entre si. A composição permite usar elementos nativos, como <select>, <details>, <form> e <video> são muito flexíveis. Cada uma dessas tags aceita determinados HTML como filhos e faz algo especial com eles. Por exemplo: <select> sabe como renderizar <option> e <optgroup> em um menu suspenso e widgets de seleção múltipla. O elemento <details> renderiza <summary> como uma seta expansível. Até mesmo a <video> sabe como lidar com determinadas crianças: Os elementos <source> não são renderizados, mas afetam o comportamento do vídeo. Que mágica!

Terminologia: light DOM x shadow DOM

A composição do Shadow DOM introduz vários conceitos básicos novos na Web no desenvolvimento de software. Antes de entrar em detalhes, vamos padronizar alguns terminologia, então usemos a mesma linguagem.

Light DOM (link em inglês)

A marcação que um usuário do seu componente grava. Este DOM fica fora do shadow DOM do componente. São os filhos reais do elemento.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Shadow DOM

O DOM que o autor do componente escreve. O Shadow DOM é local em relação ao componente e define sua estrutura interna, CSS com escopo e encapsula sua implementação detalhes. Ele também pode definir como renderizar a marcação criada pelo consumidor do seu componente.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Árvore do DOM nivelada

O resultado da distribuição do light DOM do usuário pelo navegador na sua sombra DOM, renderizando o produto final. A árvore plana é o que você finalmente percebe no DevTools e o que é renderizado na página.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

O <slot> elemento

O Shadow DOM compõe árvores do DOM diferentes usando o elemento <slot>. Os slots são espaços reservados dentro do componente que os usuários podem preencher com o marcação própria. Ao definir um ou mais slots, você convida marcações externas para renderizar no shadow DOM do seu componente. Basicamente, você diz "Renderize o conteúdo marcação aqui".

Os elementos podem "cruzar" limite do shadow DOM quando uma <slot> convida a entrada delas. Esses elementos são chamados de nós distribuídos. Conceitualmente, de nós distribuídos pode parecer um pouco estranho. Os slots não movem fisicamente o DOM. elas renderizá-lo em outro local dentro do shadow DOM.

Um componente pode definir zero ou mais slots no shadow DOM. Os slots podem ficar vazios ou fornecer conteúdo substituto. Se o usuário não fornecer o light DOM. conteúdo, o slot renderizará o conteúdo substituto.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Também é possível criar slots nomeados. Os slots nomeados são espaços específicos shadow DOM que os usuários podem referenciar pelo nome.

Exemplo: os slots no shadow DOM de <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Os usuários do componente declaram <fancy-tabs> da seguinte forma:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

E, se você está se perguntando, a árvore plana tem a seguinte aparência:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Observe que nosso componente é capaz de lidar com configurações diferentes, mas o árvore de DOM simplificada permanece a mesma. Também podemos mudar de <button> para <h2>. Esse componente foi criado para lidar com diferentes tipos de filhos... assim como <select> faz!

Estilo

Há muitas opções para estilizar componentes da Web. Componente que usa sombra O DOM pode ser estilizado pela página principal, definir seus próprios estilos ou fornecer ganchos (na a forma de propriedades personalizadas de CSS) para que os usuários modifiquem os padrões.

Estilos definidos pelo componente

De certa forma, o recurso mais útil do shadow DOM é o CSS com escopo:

  • Os seletores de CSS da página externa não se aplicam dentro do seu componente.
  • Os estilos definidos na parte interna não são vazados. Eles estão no escopo do elemento host.

Os seletores CSS usados dentro do shadow DOM se aplicam localmente ao seu componente. Em prática, isso significa que podemos usar nomes de ID/classe comuns novamente, sem nos preocuparmos sobre conflitos em outros lugares da página. Seletores CSS mais simples são uma prática recomendada dentro do Shadow DOM. Eles também são bons para o desempenho.

Exemplo: estilos definidos em uma raiz paralela são locais

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

As folhas de estilo também têm o escopo definido para a árvore paralela:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Você já se perguntou como o elemento <select> renderiza um widget de seleção múltipla (em vez de uma lista suspensa) quando você adiciona o atributo multiple:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

A <select> pode se estilizar de forma diferente com base nos atributos declarar nele. Os componentes da Web também podem aplicar estilo a si mesmos usando a classe :host seletor.

Exemplo: um componente que aplica estilo a si mesmo

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Um problema do :host é que as regras na página principal são mais específicas do que :host regras definidas no elemento. Ou seja, estilos externos vencem. Isso permite que os usuários modifiquem externamente o estilo de nível superior. Além disso, :host só funciona no contexto de uma raiz paralela. Portanto, não pode ser usada fora o shadow DOM.

A forma funcional de :host(<selector>) permite que você direcione o host se ele corresponde a um <selector>. Essa é uma ótima forma de seu componente encapsular comportamentos que reagem à interação do usuário ou declaram ou estilizam nós internos com base no host.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Estilo com base no contexto

:host-context(<selector>) corresponde ao componente se ele ou qualquer um dos ancestrais dele. corresponde a <selector>. Um uso comum para isso é aplicação de temas com base no atributo arredores. Por exemplo, muitas pessoas aplicam temas aplicando uma classe a <html> ou <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) definiria o estilo de <fancy-tabs> quando for descendente de .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() pode ser útil para a aplicação de temas, mas uma abordagem ainda melhor é criar ganchos de estilo usando propriedades personalizadas do CSS.

Como definir o estilo de nós distribuídos

::slotted(<compound-selector>) corresponde a nós que são distribuídos em um <slot>.

Vamos supor que criamos um componente de selo de nome:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

O shadow DOM do componente pode estilizar o <h2> e o .title do usuário:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Como vimos antes, <slot>s não movem o light DOM do usuário. Quando nós forem distribuídos em uma <slot>, a <slot> renderiza o DOM, mas o nós permanecem fisicamente na posição. Os estilos aplicados antes da distribuição continuam se aplicam após a distribuição. No entanto, quando o light DOM é distribuído, ele pode assumem estilos adicionais (definidos pelo shadow DOM).

Outro exemplo mais detalhado de <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

Neste exemplo, há dois espaços: um nomeado para os títulos das guias e um para o conteúdo do painel da guia. Quando o usuário seleciona uma guia, aplicamos negrito à seleção e revelar o painel. Isso é feito selecionando nós distribuídos que têm o selected. O JS do elemento personalizado (não mostrado aqui) adiciona que na hora certa.

Aplicar estilo a um componente externo

Há algumas maneiras externas de estilizar um componente. O jeito mais fácil é usar o nome da tag como seletor:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Os estilos externos sempre prevalecem sobre os estilos definidos no shadow DOM. Por exemplo: se o usuário escrever o seletor fancy-tabs { width: 500px; }, ele terá prioridade a regra do componente: :host { width: 650px;}.

Estilizar o componente em si só vai dar uma dimensão limitada. Mas o que acontece se você quer definir o estilo dos componentes internos de um componente? Para isso, precisamos de um código CSS personalizado propriedades.

Como criar ganchos de estilo usando propriedades personalizadas do CSS

Os usuários podem ajustar estilos internos se o autor do componente fornecer ganchos para aplicação de estilo usando propriedades personalizadas de CSS. Conceitualmente, a ideia é semelhante <slot>: Você cria "marcadores de estilo" para os usuários modificarem.

Exemplo: <fancy-tabs> permite que os usuários modifiquem a cor do plano de fundo:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Dentro do shadow DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

Neste caso, o componente usará black como o valor de segundo plano, uma vez que o pelo usuário que o forneceu. Caso contrário, o padrão será #9E9E9E.

Temas avançados

Criar raízes paralelas fechadas (deve evitar)

Há outro tipo de shadow DOM chamado "closed" modo Quando você cria árvore paralela fechada, o JavaScript fora do JavaScript não poderá acessar o DOM interno do seu componente. Esse processo é parecido com o funcionamento de elementos nativos, como <video>. O JavaScript não pode acessar o shadow DOM de <video> porque o navegador a implementa usando uma raiz paralela de modo fechado.

Exemplo: criar uma árvore paralela fechada:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Outras APIs também são afetadas pelo modo fechado:

  • Element.assignedSlot / TextNode.assignedSlot retorna null
  • Event.composedPath() para eventos associados a elementos dentro da sombra DOM, retorna []
.

Veja aqui um resumo dos motivos pelos quais você nunca deve criar componentes da Web com {mode: 'closed'}:

  1. Sensação artificial de segurança. Não há nada que impeça um invasor que está invadindo Element.prototype.attachShadow.

  2. O modo fechado evita que o código do elemento personalizado acesse o próprio shadow DOM correspondente. Isso é um completo fracasso. Em vez disso, você terá que guardar uma referência para depois, se quiser usar algo como querySelector(). Isso faz invalida o propósito original do modo fechado.

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. O modo fechado torna o componente menos flexível para os usuários finais. Conforme você criar componentes da Web, chegará o momento em que você se esquecerá de adicionar . Uma opção de configuração. Um caso de uso que o usuário quer. Um erro comum exemplo é esquecer de incluir ganchos de estilo adequados para nós internos. Com o modo fechado, os usuários não têm como modificar os padrões e ajustar estilos. Poder acessar os componentes internos do componente é muito útil. No final das contas, os usuários vão bifurcar seu componente, encontrar outro ou criar se ele não fizer o que eles querem :(

Como trabalhar com slots no JS

A API shadow DOM oferece utilitários para trabalhar com slots e nós. Isso é útil ao criar um elemento personalizado.

evento slotchange

O evento slotchange é disparado quando os nós distribuídos de um slot são alterados. Para exemplo, se o usuário adicionar/remover filhos do light DOM.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Para monitorar outros tipos de alterações no light DOM, você pode configurar uma MutationObserver no construtor do seu elemento.

Quais elementos estão sendo renderizados em um slot?

Às vezes, é útil saber quais elementos estão associados a um slot. Ligação slot.assignedNodes() para descobrir quais elementos o slot está renderizando. A A opção {flatten: true} também vai retornar o conteúdo substituto de um slot (se nenhum nó estão sendo distribuídas).

Como exemplo, vamos supor que o shadow DOM é semelhante a este:

<slot><b>fallback content</b></slot>
UsoLigarResultado
<my-component>texto do componente</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

A qual slot um elemento está atribuído?

Também é possível responder à pergunta inversa. element.assignedSlot diz a quais espaços de componente seu elemento está atribuído.

O modelo de eventos do Shadow DOM

Quando um evento surge do shadow DOM, seu destino é ajustado para manter a que o shadow DOM oferece. Ou seja, os eventos são segmentados novamente para parecer como se tivessem vindo do componente, e não de elementos internos dentro do seu o shadow DOM. Alguns eventos nem mesmo são propagados para fora do shadow DOM.

Os eventos que cruzam o limite da sombra são:

  • Eventos em foco: blur, focus, focusin, focusout
  • Eventos de mouse: click, dblclick, mousedown, mouseenter, mousemove etc.
  • Eventos da roda: wheel
  • Eventos de entrada: beforeinput, input
  • Eventos de teclado: keydown, keyup
  • Eventos de composição: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop etc.

Dicas

Se a árvore paralela estiver aberta, chamar event.composedPath() retornará uma matriz. de nós percorridos pelo evento.

Usar eventos personalizados

Os eventos DOM personalizados que são disparados em nós internos de uma árvore paralela não bolha fora do limite da sombra, a menos que o evento seja criado usando o Sinalização composed: true:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Se for composed: false (padrão), os consumidores não poderão detectar o evento. fora da raiz paralela.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Processamento de foco

Como você se lembra do modelo de eventos do shadow DOM, os eventos que são disparados dentro do shadow DOM são ajustados para parecer que vêm do elemento host. Por exemplo, digamos que você clique em um <input> dentro de uma raiz paralela:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

Aparentemente, o evento focus veio de <x-focus>, não de <input>. Da mesma forma, document.activeElement será <x-focus>. Se a raiz paralela foi criada com mode:'open' (consulte modo fechado), você também o nó interno que ganhou o foco:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Se houver vários níveis de shadow DOM em jogo (digamos, um elemento personalizado no outro elemento personalizado), será necessário detalhar recursivamente as raízes paralelas para encontre o activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Outra opção de foco é delegatesFocus: true, que expande a comportamento de foco do elemento dentro de uma árvore paralela:

  • Se você clicar em um nó dentro do shadow DOM e o nó não for uma área focalizável, a primeira área focalizável fica focada.
  • Quando um nó dentro do shadow DOM ganha o foco, o :focus é aplicado ao host em uma adição ao elemento em foco.

Exemplo: como delegatesFocus: true muda o comportamento de foco

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Result

delegatesFocus: comportamento verdadeiro.

Acima está o resultado quando <x-focus> está em foco (clique do usuário, guia para focus() etc.), "Texto clicável do Shadow DOM" é clicado, ou o <input> está em foco (incluindo autofocus).

Se você definisse delegatesFocus: false, veja o que seria exibido:

delegatesFocus: false e a entrada interna será focada.
delegatesFocus: false, e o <input> interno está em foco.
.
delegatesFocus: falso e foco x
    recebe foco (por exemplo, tem tabindex=&#39;0&#39;).
delegatesFocus: false e <x-focus> recebe foco (por exemplo, tem tabindex="0").
delegatesFocus: false e &quot;Texto clicável do Shadow DOM&quot; é
    clicado (ou outra área vazia no shadow DOM do elemento é clicada).
delegatesFocus: false e "Texto clicável do Shadow DOM" é clicado (ou outra área vazia no shadow DOM do elemento é clicada).

Dicas e sugestões

Ao longo dos anos, aprendi algumas coisas sobre a criação de componentes da Web. eu acho que você achará algumas dessas dicas úteis para a criação de componentes e depuração do shadow DOM.

Usar contenção do CSS

Normalmente, o layout/estilo/pintura de um componente da Web é razoavelmente independente. Usar Contenção de CSS em :host para um desempenho vencer:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Como redefinir estilos herdáveis

Os estilos herdáveis (background, color, font, line-height etc.) continuam herdar no shadow DOM. Ou seja, eles cruzam o limite do shadow DOM padrão. Se você quiser começar do zero, use all: initial; para redefinir os estilos herdáveis ao seu valor inicial quando cruzam o limite da sombra.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Como encontrar todos os elementos personalizados usados por uma página

Às vezes, é útil encontrar elementos personalizados usados na página. Para isso, você precisa percorrer recursivamente o shadow DOM de todos os elementos usados na página.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Criação de elementos usando um <template>

Em vez de preencher uma raiz paralela usando .innerHTML, podemos usar uma <template>. Os modelos são um marcador de posição ideal para declarar a estrutura de um componente da Web.

Confira o exemplo no "Elementos personalizados: criar componentes da Web reutilizáveis".

História e compatibilidade com navegadores

Se você acompanhou os componentes da Web nos últimos dois anos, que o Chrome 35+/Opera esteja lançando uma versão mais antiga do shadow DOM há algum tempo. O Blink vai continuar a oferecer suporte às duas versões em paralelo por alguns tempo de resposta. A especificação v0 oferecia um método diferente para criar uma raiz paralela. (element.createShadowRoot em vez de element.attachShadow da v1). Chamar o o método mais antigo continua criando uma raiz paralela com semântica da v0. Portanto, a v0 código não vai quebrar.

Se você estiver interessado na antiga especificação v0, confira a documentação do html5rocks artigos: 1 2, 3. Há também uma ótima comparação diferenças entre o shadow DOM v0 e o v1.

Suporte ao navegador

O Shadow DOM v1 é fornecido no Chrome 53 (status), Opera 40, Safari 10 e Firefox 63. Borda começou o desenvolvimento.

Para detectar o shadow DOM, verifique a existência de attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Até que o suporte ao navegador esteja amplamente disponível, a shadydom e Os polyfills shadycss oferecem a v1 . O Shady DOM imita o escopo do DOM do Shadow DOM e os polyfills do shadycss Propriedades personalizadas de CSS e o escopo de estilo fornecido pela API nativa.

Instale os polyfills:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Use os polyfills:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Consulte a documentação https://github.com/webcomponents/shadycss#usage para obter instruções sobre como reduzir o escopo de seus estilos.

Conclusão

Pela primeira vez, temos um primitivo de API que faz um escopo adequado de CSS, escopo do DOM e tem uma composição verdadeira. Combinadas com outras APIs de componentes da Web assim como os elementos personalizados, o shadow DOM permite criar elementos componentes sem hackers ou usando bagagens mais antigas, como <iframe>s.

Não me entenda mal. O Shadow DOM com certeza é muito complexo. Mas é uma fera que vale a pena aprender. Passe algum tempo com isso. Aprenda e faça perguntas!

Leitura adicional

Perguntas frequentes

Posso usar o Shadow DOM v1 hoje?

Com um polyfill, sim. Consulte Compatibilidade do navegador.

Quais recursos de segurança o shadow DOM oferece?

O Shadow DOM não é um recurso de segurança. É uma ferramenta leve para definir o escopo do CSS e ocultar árvores do DOM no componente. Se você quiser um limite de segurança real, use um <iframe>.

Um componente da Web precisa usar o shadow DOM?

Não. Não é necessário criar componentes da Web que usam o shadow DOM. No entanto, a criação de elementos personalizados que usam o Shadow DOM significa que você pode aproveitar a vantagem de recursos como escopo de CSS, encapsulamento de DOM e composição.

Qual é a diferença entre raízes paralelas abertas e fechadas?

Consulte Raízes paralelas fechadas.