Shadow DOM, szablony i gniazda

Funkcje internetowe: Shadow DOM <slot>

Korzeń shadow to hermetyzowane poddrzewo DOM dołączone do elementu hosta: style wewnątrz niego nie wyciekają na zewnątrz, a style strony nie wyciekają do środka (z wyjątkiem właściwości dziedziczonych). Szablony skutecznie tworzą drzewo, a przedziały wyświetlają w nim treść Light DOM.

Dołączanie korzenia shadow

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

delegatesFocus: true sprawia, że zaznaczenie hosta powoduje przeniesienie zaznaczenia na pierwszy element możliwy do zaznaczenia w drzewie shadow, przekierowuje stylizację :focus do hosta, a w przypadku elementów niestandardowych powiązanych z formularzem (static formAssociated = true) przekazuje kliknięcia <label> do wewnętrznego elementu sterującego. Ustawiaj go zawsze, gdy komponent zawiera elementy sterujące, które można zaznaczyć. W niemal wszystkich przypadkach używaj mode: 'open', aby narzędzia dla programistów, skrypty stron i hydratacja serwera mogły dotrzeć do element.shadowRoot (patrz Web Components Orientation).

Szablony: efektywne tworzenie drzewa

Zdefiniuj <template> raz (zakres modułu lub pole statyczne) i sklonuj go dla każdej instancji. Klonowanie przeanalizowanego fragmentu jest znacznie szybsze niż ponowna analiza ciągu znaków innerHTML przy każdym tworzeniu.

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);

Szablony powinny być proste. Głęboko zagnieżdżone elementy div i wiele poziomów zagnieżdżonych elementów shadow root zwiększają koszty związane z rozpoznawaniem stylów i układem, co ma duży wpływ na wydajność w przypadku dużej skali. Zastosuj płaską strukturę i elementy semantyczne.

Automaty: elementy, które zaskakują ludzi

Domyślne i nazwane boksy działają zgodnie z dokumentacją. Oto zachowania, które mogą sprawiać problemy:

  • Spacje mają znaczenie. Węzły tekstowe między elementami (w tym same odstępy) są przypisywane do domyślnego boksu, więc domyślny boks rzadko jest naprawdę „pusty”.
  • Zachowywana jest kolejność DOM, a nie kolejność boksów. Węzły rzutowane zachowują pierwotną kolejność Light DOM na potrzeby :first-child/:last-child, niezależnie od tego, który slot wypełniają. Reguła ::slotted(*:first-child) jest kierowana na pierwszy element podrzędny DOM, a nie na pierwszy element podrzędny tego boksu, więc często nie będzie pasować do tego, czego oczekujesz.
  • slotchange to sygnał dotyczący czasu uaktualnienia. Zdarzenie to jest wywoływane, gdy zmieniają się przypisane węzły (w tym początkowe przypisanie). Jest to niezawodny sposób na sprawdzenie, czy elementy podrzędne Light DOM zostały dodane po uaktualnieniu, gdy mogły nie istnieć podczas connectedCallback.
this.shadowRoot.querySelector('slot[name="header"]')
  .addEventListener('slotchange', (e) => {
    const nodes = e.target.assignedElements();
    this.toggleAttribute('has-header', nodes.length > 0);
  });

Wczytywanie arkuszy stylów do elementu shadow root

Metoda Zalety Wady
<style> w szablonie Prosty, w pełni hermetyczny, działa bez JS. Duplikowane dla każdej instancji; zwiększa rozmiar DOM w skali.
<link rel="stylesheet"> Plik zewnętrzny, który można zapisać w pamięci podręcznej. Dodatkowe żądanie; ryzyko wyświetlenia treści bez stylów.
adoptedStyleSheets Jedna współdzielona CSSStyleSheet we wszystkich instancjach; najszybsza przy dużej skali. Wymaga JS; utworzone arkusze nie obsługują @import.

W przypadku wielu instancji jednego komponentu preferowany jest udostępniony utworzony arkusz stylów:

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];
  }
}