Stínové DOM sloty, kompozice

Mnoho typů komponent, jako jsou karty, nabídky, galerie obrázků a tak dále, potřebuje obsah k vykreslení.

Stejně jako vestavěný prohlížeč <select> očekává <option> položky, naše <custom-tabs> může očekávat, že bude předán skutečný obsah karty. A <custom-menu> může očekávat položky nabídky.

Kód, který využívá <custom-menu> může vypadat takto:

<custom-menu>
  <title>Candy menu</title>
  <item>Lollipop</item>
  <item>Fruit Toast</item>
  <item>Cup Cake</item>
</custom-menu>

…Pak by to naše komponenta měla vykreslit správně, jako pěkné menu s daným názvem a položkami, zpracovávat události menu atd.

Jak to implementovat?

Mohli bychom zkusit analyzovat obsah prvku a dynamicky zkopírovat a přeuspořádat uzly DOM. To je možné, ale pokud přesouváme prvky do stínového DOM, pak se tam styly CSS z dokumentu nepoužijí, takže se vizuální styl může ztratit. To také vyžaduje určité kódování.

Naštěstí nemusíme. Shadow DOM podporuje <slot> prvky, které jsou automaticky vyplněny obsahem z lehkého DOM.

Pojmenované bloky

Podívejme se, jak sloty fungují na jednoduchém příkladu.

Zde <user-card> stínový DOM poskytuje dva sloty, vyplněné světlým DOM:

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <div>Name:
        <slot name="username"></slot>
      </div>
      <div>Birthday:
        <slot name="birthday"></slot>
      </div>
    `;
  }
});
</script>

<user-card>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
</user-card>

Ve stínovém DOM <slot name="X"> definuje „bod vložení“, místo, kde jsou prvky s slot="X" jsou vykresleny.

Poté prohlížeč provede „kompozici“:vezme prvky ze světlého DOM a vykreslí je v odpovídajících slotech stínového DOM. Na konci máme přesně to, co chceme – komponentu, kterou lze naplnit daty.

Zde je struktura DOM za skriptem, která nebere v úvahu složení:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username"></slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
</user-card>

Vytvořili jsme stínový DOM, takže tady je pod #shadow-root . Nyní má prvek DOM světla i stínu.

Pro účely vykreslování pro každý <slot name="..."> ve stínovém DOM prohlížeč hledá slot="..." se stejným názvem ve světlém DOM. Tyto prvky jsou vykresleny uvnitř slotů:

Výsledek se nazývá „zploštělý“ DOM:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        <!-- slotted element is inserted into the slot -->
        <span slot="username">John Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday">
        <span slot="birthday">01.01.2001</span>
      </slot>
    </div>
</user-card>

…Ale sloučený DOM existuje pouze pro účely vykreslování a zpracování událostí. Je to tak trochu „virtuální“. Tak se věci ukazují. Ale uzly v dokumentu se ve skutečnosti nepohybují!

To lze snadno zkontrolovat, když spustíme querySelectorAll :uzly jsou stále na svých místech.

// light DOM <span> nodes are still at the same place, under `<user-card>`
alert( document.querySelectorAll('user-card span').length ); // 2

Zploštělý DOM je tedy odvozen od stínového DOM vložením slotů. Prohlížeč jej vykreslí a použije pro dědění stylů, šíření událostí (o tom později). JavaScript však stále vidí dokument „tak, jak je“, před zploštěním.

Pouze děti nejvyšší úrovně mohou mít atribut slot="…".

slot="..." Atribut je platný pouze pro přímé potomky stínového hostitele (v našem příkladu <user-card> živel). U vnořených prvků se ignoruje.

Například druhý <span> zde je ignorováno (protože se nejedná o potomka nejvyšší úrovně <user-card> ):

<user-card>
  <span slot="username">John Smith</span>
  <div>
    <!-- invalid slot, must be direct child of user-card -->
    <span slot="birthday">01.01.2001</span>
  </div>
</user-card>

Pokud je v light DOM více prvků se stejným názvem slotu, jsou do slotu připojeny jeden po druhém.

Například toto:

<user-card>
  <span slot="username">John</span>
  <span slot="username">Smith</span>
</user-card>

Dává tento zploštělý DOM se dvěma prvky v <slot name="username"> :

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        <span slot="username">John</span>
        <span slot="username">Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
</user-card>

Záložní obsah bloku

Pokud něco vložíme do <slot> , stane se záložním, „výchozím“ obsahem. Prohlížeč to zobrazí, pokud v light DOM není žádná odpovídající výplň.

Například v této části stínového DOM Anonymous vykreslí, pokud neexistuje slot="username" ve světle DOM.

<div>Name:
  <slot name="username">Anonymous</slot>
</div>

Výchozí blok:první bez názvu

První <slot> ve stínovém DOM, který nemá název, je „výchozí“ slot. Získává všechny uzly z lehkého DOM, které nejsou umístěny jinde.

Například do <user-card> přidejte výchozí slot který zobrazuje všechny informace o uživateli bez slotu:

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
    <div>Name:
      <slot name="username"></slot>
    </div>
    <div>Birthday:
      <slot name="birthday"></slot>
    </div>
    <fieldset>
      <legend>Other information</legend>
      <slot></slot>
    </fieldset>
    `;
  }
});
</script>

<user-card>
  <div>I like to swim.</div>
  <span slot="username">John Smith</span>
  <span slot="birthday">01.01.2001</span>
  <div>...And play volleyball too!</div>
</user-card>

Veškerý volný obsah DOM se dostane do sady polí „Další informace“.

Prvky se do slotu připojují jeden po druhém, takže obě informace bez slotu jsou ve výchozím slotu společně.

Zploštělý DOM vypadá takto:

<user-card>
  #shadow-root
    <div>Name:
      <slot name="username">
        <span slot="username">John Smith</span>
      </slot>
    </div>
    <div>Birthday:
      <slot name="birthday">
        <span slot="birthday">01.01.2001</span>
      </slot>
    </div>
    <fieldset>
      <legend>Other information</legend>
      <slot>
        <div>I like to swim.</div>
        <div>...And play volleyball too!</div>
      </slot>
    </fieldset>
</user-card>

Příklad nabídky

Nyní se vraťme k <custom-menu> , zmíněný na začátku kapitoly.

K distribuci prvků můžeme použít sloty.

Zde je označení pro <custom-menu> :

<custom-menu>
  <span slot="title">Candy menu</span>
  <li slot="item">Lollipop</li>
  <li slot="item">Fruit Toast</li>
  <li slot="item">Cup Cake</li>
</custom-menu>

Šablona stínového DOM se správnými sloty:

<template id="tmpl">
  <style> /* menu styles */ </style>
  <div class="menu">
    <slot name="title"></slot>
    <ul><slot name="item"></slot></ul>
  </div>
</template>
  1. <span slot="title"> přejde do <slot name="title"> .
  2. Existuje mnoho <li slot="item"> v <custom-menu> , ale pouze jeden <slot name="item"> v šabloně. Takže všechny takové <li slot="item"> jsou připojeny k <slot name="item"> jeden po druhém, čímž tvoří seznam.

Zploštělý DOM se změní na:

<custom-menu>
  #shadow-root
    <style> /* menu styles */ </style>
    <div class="menu">
      <slot name="title">
        <span slot="title">Candy menu</span>
      </slot>
      <ul>
        <slot name="item">
          <li slot="item">Lollipop</li>
          <li slot="item">Fruit Toast</li>
          <li slot="item">Cup Cake</li>
        </slot>
      </ul>
    </div>
</custom-menu>

Někdo si může všimnout, že v platném DOM je <li> musí být přímým potomkem <ul> . Ale to je zploštělý DOM, popisuje, jak se komponenta vykresluje, taková věc se zde přirozeně stává.

Potřebujeme pouze přidat click handler pro otevření/zavření seznamu a <custom-menu> je připraven:

customElements.define('custom-menu', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});

    // tmpl is the shadow DOM template (above)
    this.shadowRoot.append( tmpl.content.cloneNode(true) );

    // we can't select light DOM nodes, so let's handle clicks on the slot
    this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
      // open/close the menu
      this.shadowRoot.querySelector('.menu').classList.toggle('closed');
    };
  }
});

Zde je úplná ukázka:

Můžeme k tomu samozřejmě přidat další funkce:události, metody a tak dále.

Aktualizace slotů

Co když chce vnější kód dynamicky přidávat/odebírat položky nabídky?

Prohlížeč sleduje bloky a aktualizuje vykreslování, pokud jsou přidány nebo odebrány prvky s bloky.

Vzhledem k tomu, že se lehké uzly DOM nekopírují, ale pouze vykreslují ve slotech, změny v nich jsou okamžitě viditelné.

Pro aktualizaci vykreslování tedy nemusíme nic dělat. Ale pokud chce kód komponenty vědět o změnách slotu, pak slotchange událost je k dispozici.

Zde je například položka nabídky vložena dynamicky po 1 sekundě a název se změní po 2 sekundách:

<custom-menu id="menu">
  <span slot="title">Candy menu</span>
</custom-menu>

<script>
customElements.define('custom-menu', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div class="menu">
      <slot name="title"></slot>
      <ul><slot name="item"></slot></ul>
    </div>`;

    // shadowRoot can't have event handlers, so using the first child
    this.shadowRoot.firstElementChild.addEventListener('slotchange',
      e => alert("slotchange: " + e.target.name)
    );
  }
});

setTimeout(() => {
  menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>')
}, 1000);

setTimeout(() => {
  menu.querySelector('[slot="title"]').innerHTML = "New menu";
}, 2000);
</script>

Vykreslování nabídky se aktualizuje pokaždé bez našeho zásahu.

Existují dva slotchange události zde:

  1. Při inicializaci:

    slotchange: title spustí se okamžitě, jako slot="title" ze světelného DOM se dostane do odpovídajícího slotu.

  2. Po 1 sekundě:

    slotchange: item spustí, když se objeví nový <li slot="item"> je přidáno.

Poznámka:neexistuje žádné slotchange událost po 2 sekundách, kdy je obsah slot="title" je upraven. Je to proto, že nedochází ke změně slotu. Upravujeme obsah uvnitř štěrbinového prvku, to je další věc.

Pokud bychom chtěli sledovat interní modifikace lehkého DOM z JavaScriptu, je to možné také pomocí obecnějšího mechanismu:MutationObserver.

Rozhraní API pro slot

Nakonec zmiňme metody JavaScriptu související s bloky.

Jak jsme viděli dříve, JavaScript se dívá na „skutečný“ DOM bez zploštění. Ale pokud má stínový strom {mode: 'open'} , pak můžeme zjistit, které prvky jsou přiřazeny ke slotu a naopak slotu prvkem uvnitř něj:

  • node.assignedSlot – vrátí <slot> prvek, který node je přiřazeno.
  • slot.assignedNodes({flatten: true/false}) – DOM uzly přiřazené k slotu. flatten možnost je false ve výchozím stavu. Pokud je explicitně nastaveno na true , pak se podívá hlouběji na zploštělý DOM a vrací vnořené sloty v případě vnořených komponent a záložní obsah, pokud není přiřazen žádný uzel.
  • slot.assignedElements({flatten: true/false}) – Prvky DOM přiřazené k slotu (stejné jako výše, ale pouze uzly prvků).

Tyto metody jsou užitečné, když potřebujeme obsah v blocích nejen zobrazit, ale také jej sledovat v JavaScriptu.

Pokud například <custom-menu> komponenta chce vědět, co zobrazuje, pak by mohla sledovat slotchange a získejte položky z slot.assignedElements :

<custom-menu id="menu">
  <span slot="title">Candy menu</span>
  <li slot="item">Lollipop</li>
  <li slot="item">Fruit Toast</li>
</custom-menu>

<script>
customElements.define('custom-menu', class extends HTMLElement {
  items = []

  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div class="menu">
      <slot name="title"></slot>
      <ul><slot name="item"></slot></ul>
    </div>`;

    // triggers when slot content changes
    this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
      let slot = e.target;
      if (slot.name == 'item') {
        this.items = slot.assignedElements().map(elem => elem.textContent);
        alert("Items: " + this.items);
      }
    });
  }
});

// items update after 1 second
setTimeout(() => {
  menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Cup Cake</li>')
}, 1000);
</script>

Shrnutí

Obvykle, pokud má prvek stínový DOM, pak se jeho světlý DOM nezobrazí. Sloty umožňují zobrazit prvky ze světlého DOM v určených místech stínového DOM.

Existují dva druhy slotů:

  • Pojmenované bloky:<slot name="X">...</slot> – získá lehké děti s slot="X" .
  • Výchozí blok:první <slot> bez jména (následné nepojmenované sloty jsou ignorovány) – získá nezařazené lehké děti.
  • Pokud je pro stejný blok mnoho prvků, připojují se jeden po druhém.
  • Obsah <slot> prvek se používá jako záložní. Zobrazuje se, pokud pro slot nejsou žádné lehké děti.

Proces vykreslování štěrbinových prvků uvnitř jejich štěrbin se nazývá „kompozice“. Výsledek se nazývá „zploštělý DOM“.

Kompozice ve skutečnosti nehýbe uzly, z pohledu JavaScriptu je DOM stále stejný.

JavaScript může přistupovat ke slotům pomocí metod:

  • slot.assignedNodes/Elements() – vrátí uzly/prvky uvnitř slot .
  • node.assignedSlot – vlastnost reverse, vrací slot pomocí uzlu.

Pokud bychom chtěli vědět, co zobrazujeme, můžeme sledovat obsah slotu pomocí:

  • slotchange událost – spustí se při prvním zaplnění slotu a při jakékoli operaci přidání/odebrání/nahrazení prvku s drážkou, ale ne jeho potomků. Slot je event.target .
  • MutationObserver, chcete-li jít hlouběji do obsahu slotu, sledujte změny v něm.

Nyní, když víme, jak zobrazit prvky ze světlého DOM ve stínovém DOM, pojďme se podívat, jak je správně stylovat. Základním pravidlem je, že stínové prvky jsou stylizovány uvnitř a světlé prvky – venku, ale existují významné výjimky.

Podrobnosti uvidíme v další kapitole.