Mnoho druhů komponent, napÅÃklad záložky, menu, obrázkové galerie a podobnÄ, potÅebuje vykreslit obsah.
StejnÄ jako zabudovaná prohlÞeÄová znaÄka <select> oÄekává položky <option>, naÅ¡e <vlastni-zalozky> mohou oÄekávat, že do nich bude pÅedán skuteÄný obsah záložek. A <vlastni-menu> může oÄekávat položky menu.
Kód, který využÃvá <vlastni-menu>, může vypadat následovnÄ:
<vlastni-menu>
<title>NabÃdka sladkostÃ</title>
<item>LÃzátko</item>
<item>Ovocný toast</item>
<item>KoÅ¡ÃÄky</item>
</vlastni-menu>
â¦Pak by je naÅ¡e komponenta mÄla správnÄ vykreslit jako pÄkné menu se zadaným titulkem a položkami, zpracovávat události menu a tak dále.
Jak to implementovat?
Můžeme se pokusit analyzovat obsah elementu a dynamicky zkopÃrovat a pÅeskládat DOM uzly. To je možné, ale když budeme pÅesunovat elementy do stÃnového DOMu, nebudou se v nich aplikovat CSS styly z dokumentu, takže můžeme ztratit vizuálnà stylizaci. NavÃc to vyžaduje napsat nÄjaký ten kód.
NaÅ¡tÄstà to nemusÃme dÄlat. StÃnový DOM podporuje elementy <slot>, které se automaticky naplnà obsahem ze svÄtlého DOMu.
Pojmenované sloty
Na jednoduchém pÅÃkladu se podÃvejme, jak sloty fungujÃ.
Zde stÃnový DOM znaÄky <karta-uzivatele> poskytuje dva sloty, naplÅované ze svÄtlého DOMu:
<script>
customElements.define('karta-uzivatele', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Jméno:
<slot name="uživatel"></slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ"></slot>
</div>
`;
}
});
</script>
<karta-uzivatele>
<span slot="uživatel">Jan Novák</span>
<span slot="narozenÃ">01.01.2001</span>
</karta-uzivatele>
Ve stÃnovém DOMu <slot name="X"> definuje âbod vloženÃâ, mÃsto, kam budou vykresleny elementy obsahujÃcà slot="X".
ProhlÞeÄ pak provede âkompoziciâ: vezme elementy ze svÄtlého DOMu a vykreslà je do odpovÃdajÃcÃch slotů stÃnového DOMu. Na konci budeme mÃt pÅesnÄ to, co jsme chtÄli â komponentu, která může být vyplnÄna daty.
Zde je struktura DOMu po provedenà skriptu, nebereme-li v úvahu kompozici:
<karta-uzivatele>
#shadow-root
<div>Jméno:
<slot name="uživatel"></slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ"></slot>
</div>
<span slot="uživatel">Jan Novák</span>
<span slot="narozenÃ">01.01.2001</span>
</karta-uzivatele>
VytvoÅili jsme stÃnový DOM, takže je zde pod #shadow-root. Nynà element obsahuje svÄtlý i stÃnový DOM.
Pro úÄely vykreslenà prohlÞeÄ pro každou znaÄku <slot name="..."> ve stÃnovém DOMu vyhledá slot="..." se stejným názvem ve svÄtlém DOMu. Tyto elementy jsou vykresleny do slotů:
Výsledek se nazývá âzploÅ¡tÄlýâ (flattened) DOM:
<karta-uzivatele>
#shadow-root
<div>Jméno:
<slot name="uživatel">
<!-- do slotu je vložen element se slotem -->
<span slot="uživatel">Jan Novák</span>
</slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ">
<span slot="narozenÃ">01.01.2001</span>
</slot>
</div>
</karta-uzivatele>
â¦ZploÅ¡tÄlý DOM vÅ¡ak existuje výhradnÄ pro úÄely vykreslovánà a zpracovánà událostÃ. Je svým způsobem âvirtuálnÃâ. Je to způsob, jak se vÅ¡e zobrazuje. Ale ve skuteÄnosti nejsou uzly v dokumentu pÅesunuty!
To si můžeme snadno zkontrolovat, jestliže spustÃme querySelectorAll: uzly budou stále na svých mÃstech.
// uzly <span> svÄtlého DOMu jsou stále na stejných mÃstech, pod `<karta-uzivatele>`
alert( document.querySelectorAll('karta-uzivatele span').length ); // 2
ZploÅ¡tÄlý DOM je tedy odvozen ze stÃnového DOMu vloženÃm slotů. ProhlÞeÄ jej vykreslà a použije pro zdÄdÄnà stylů a propagaci událostà (vÃce o tom pozdÄji). JavaScript vÅ¡ak stále vidà dokument tak, âjak jeâ, pÅed zploÅ¡tÄnÃm.
Atribut slot="..." platà jen u pÅÃmých dÄtà stÃnového hostitele (v naÅ¡em pÅÃkladu elementu <karta-uzivatele>). U vnoÅených elementů je ignorován.
NapÅÃklad druhý <span> zde je ignorován (protože to nenà dÃtÄ <karta-uzivatele> nejvyššà úrovnÄ):
<karta-uzivatele>
<span slot="uživatel">Jan Novák</span>
<div>
<!-- nesprávný slot, musà být pÅÃmým dÃtÄtem elementu karta-uzivatele -->
<span slot="narozenÃ">01.01.2001</span>
</div>
</karta-uzivatele>
Pokud je ve svÄtlém DOMu vÃce elementů se stejným názvem slotu, budou vloženy do slotu jeden po druhém.
NapÅÃklad tohle:
<karta-uzivatele>
<span slot="uživatel">Jan</span>
<span slot="uživatel">Novák</span>
</karta-uzivatele>
vydá následujÃcà zploÅ¡tÄlý DOM se dvÄma elementy ve <slot name="uživatel">:
<karta-uzivatele>
#shadow-root
<div>Jméno:
<slot name="uživatel">
<span slot="uživatel">Jan</span>
<span slot="uživatel">Novák</span>
</slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ"></slot>
</div>
</karta-uzivatele>
Záložnà obsah slotu
Pokud vložÃme nÄco do znaÄky <slot>, stane se to záložnÃm, âstandardnÃmâ obsahem. ProhlÞeÄ to zobrazÃ, jestliže svÄtlý DOM nebude obsahovat odpovÃdajÃcà náplÅ.
NapÅÃklad v tomto kousku stÃnového DOMu se zobrazà Anonym, jestliže ve svÄtlém DOMu nebude žádný slot="uživatel".
<div>Jméno:
<slot name="uživatel">Anonym</slot>
</div>
Standardnà slot: prvnà nepojmenovaný
Prvnà <slot> ve stÃnovém DOMu, který nemá žádný název, je âstandardnÃâ slot. ZÃská vÅ¡echny uzly ze svÄtlého DOMu, které nejsou vloženy do jiného slotu.
PÅidejme napÅÃklad do naÅ¡eho elementu <karta-uzivatele> standardnà slot, který zobrazà vÅ¡echny informace o uživateli, které nebyly vloženy do slotů:
<script>
customElements.define('karta-uzivatele', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Jméno:
<slot name="uživatel"></slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ"></slot>
</div>
<fieldset>
<legend>Dalšà informace</legend>
<slot></slot>
</fieldset>
`;
}
});
</script>
<karta-uzivatele>
<div>Rád plavu.</div>
<span slot="uživatel">Jan Novák</span>
<span slot="narozenÃ">01.01.2001</span>
<div>...A taky hraji volejbal!</div>
</karta-uzivatele>
VeÅ¡kerý obsah svÄtlého DOMu, který nebyl vložen do slotů, se dostane do sady polà âDalšà informaceâ.
Elementy se do slotu vkládajà jeden za druhým, takže oba nevložené kousky informace budou spoleÄnÄ ve standardnÃm slotu.
ZploÅ¡tÄlý DOM vypadá následovnÄ:
<karta-uzivatele>
#shadow-root
<div>Name:
<slot name="uživatel">
<span slot="uživatel">Jan Novák</span>
</slot>
</div>
<div>Datum narozenÃ:
<slot name="narozenÃ">
<span slot="narozenÃ">01.01.2001</span>
</slot>
</div>
<fieldset>
<legend>Dalšà informace</legend>
<slot>
<div>Rád plavu.</div>
<div>...A taky hraji volejbal!</div>
</slot>
</fieldset>
</karta-uzivatele>
PÅÃklad menu
Nynà se vraÅ¥me k <vlastni-menu>, zmÃnÄnému na zaÄátku kapitoly.
K pÅedánà elementů můžeme použÃt sloty.
Zde je kód pro <vlastni-menu>:
<vlastni-menu>
<span slot="titulek">NabÃdka sladkostÃ</span>
<li slot="položka">LÃzátko</li>
<li slot="položka">Ovocný toast</li>
<li slot="položka">KoÅ¡ÃÄky</li>
</vlastni-menu>
Å ablona stÃnového DOMu s pÅÃsluÅ¡nými sloty:
<template id="šablona">
<style> /* styly menu */ </style>
<div class="menu">
<slot name="titulek"></slot>
<ul><slot name="položka"></slot></ul>
</div>
</template>
<span slot="titulek">bude vložen do<slot name="titulek">.- V elementu
<vlastni-menu>je mnoho položek<li slot="položka">, ale v Å¡ablonÄ je jen jedna<slot name="položka">. VÅ¡echny tyto elementy<li slot="položka">se tedy vložà do<slot name="položka">jeden po druhém, ÄÃmž vytvoÅà seznam.
ZploÅ¡tÄlý DOM bude vypadat takto:
<vlastni-menu>
#shadow-root
<style> /* styly menu */ </style>
<div class="menu">
<slot name="titulek">
<span slot="titulek">NabÃdka sladkostÃ</span>
</slot>
<ul>
<slot name="položka">
<li slot="položka">LÃzátko</li>
<li slot="položka">Ovocný toast</li>
<li slot="položka">KoÅ¡ÃÄky</li>
</slot>
</ul>
</div>
</vlastni-menu>
Můžeme si vÅ¡imnout, že v platném DOMu <li> musà být pÅÃmým dÃtÄtem <ul>, ale tohle je zploÅ¡tÄlý DOM, který popisuje, jak budou komponenty vykresleny, takže takové vÄci se tady bÄžnÄ stávajÃ.
PotÅebujeme už jen pÅidat handler click k otevÅenà a zavÅenà seznamu a <vlastni-menu> je hotové:
customElements.define('vlastni-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
// Å¡ablona je Å¡ablona stÃnového DOMu (výše)
this.shadowRoot.append( šablona.content.cloneNode(true) );
// nemůžeme vybÃrat uzly svÄtlého DOMu, takže budeme zpracovávat kliknutà na slot
this.shadowRoot.querySelector('slot[name="titulek"]').onclick = () => {
// otevÅe/zavÅe menu
this.shadowRoot.querySelector('.menu').classList.toggle('closed');
};
}
});
Zde je celá ukázka:
SamozÅejmÄ do nà můžeme pÅidávat dalšà funkcionalitu: události, metody a podobnÄ.
Aktualizace slotů
Co když vnÄjšà kód chce dynamicky pÅidávat a odstraÅovat položky menu?
ProhlÞeÄ sleduje sloty a aktualizuje vykreslenÃ, pokud jsou pÅidány nebo odstranÄny elementy do slotů.
NavÃc, protože uzly svÄtlého DOMu se nekopÃrujÃ, ale jen vykreslujà do slotů, zmÄny uvnitÅ nich budou okamžitÄ viditelné.
Pro aktualizaci vykreslovánà tedy nemusÃme dÄlat nic. Jestliže vÅ¡ak kód komponenty chce vÄdÄt o zmÄnách slotů, má k dispozici událost slotchange.
NapÅÃklad zde je po 1 sekundÄ dynamicky vložena položka menu a po 2 sekundách se zmÄnà titulek:
<vlastni-menu id="menu">
<span slot="titulek">NabÃdka sladkostÃ</span>
</vlastni-menu>
<script>
customElements.define('vlastni-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="titulek"></slot>
<ul><slot name="položka"></slot></ul>
</div>`;
// shadowRoot nemůže mÃt handlery událostÃ, použijeme tedy prvnà dÃtÄ
this.shadowRoot.firstElementChild.addEventListener('slotchange',
e => alert("slotchange: " + e.target.name)
);
}
});
setTimeout(() => {
menu.insertAdjacentHTML('beforeEnd', '<li slot="položka">LÃzátko</li>')
}, 1000);
setTimeout(() => {
menu.querySelector('[slot="titulek"]').innerHTML = "Nové menu";
}, 2000);
</script>
Vykreslovánà menu se pokaždé aktualizuje bez našeho zásahu.
Nastanou tady dvÄ události slotchange:
-
PÅi inicializaci:
slotchange: titulekse spustà okamžitÄ, když seslot="titulek"ze svÄtlého DOMu vložà do pÅÃsluÅ¡ného slotu. -
Po 1 sekundÄ:
slotchange: položkase spustÃ, když je pÅidána nová<li slot="položka">.
ProsÃme vÅ¡imnÄte si, že po 2 sekundách, když se zmÄnà obsah slot="titulek", událost slotchange nenastane. Je to proto, že pÅi tom nedojde ke zmÄnÄ slotu. Modifikujeme obsah elementu vloženého do slotu, to je nÄco jiného.
Kdybychom chtÄli v JavaScriptu sledovat vnitÅnà modifikace svÄtlého DOMu, je to rovnÄž možné pomocà obecnÄjÅ¡Ãho mechanismu: MutationObserver.
API pro sloty
Na závÄr uveÄme metody JavaScriptu pro práci se sloty.
Jak jsme již vidÄli, JavaScript se dÃvá na âskuteÄnýâ, nezploÅ¡tÄlý DOM. Pokud vÅ¡ak stÃnový strom má {mode: 'open'}, můžeme zjistit, které elementy jsou pÅiÅazeny do slotu, a naopak, najÃt slot podle elementu uvnitÅ nÄj:
uzel.assignedSlotâ vrátà element<slot>, do nÄhož je pÅiÅazenuzel.slot.assignedNodes({flatten: true/false})â DOM uzly, pÅiÅazené do slotu. Volbaflattenje standardnÄfalse. Pokud je výslovnÄ nastavena natrue, metoda se podÃvá hloubÄji do zploÅ¡tÄlého DOMu, v pÅÃpadÄ vnoÅených komponent vrátà vnoÅené sloty a nenÃ-li žádný uzel pÅiÅazen, vrátà záložnà obsah.slot.assignedElements({flatten: true/false})â DOM elementy pÅiÅazené do slotu (totéž jako pÅedchozà metoda, ale jen elementové uzly).
Tyto metody jsou užiteÄné, když chceme obsah vložený do slotů nejen zobrazovat, ale i zpracovávat v JavaScriptu.
NapÅÃklad jestliže komponenta <vlastni-menu> chce vÄdÄt, co zobrazuje, může sledovat slotchange a zÃskat položky ze slot.assignedElements:
<vlastni-menu id="menu">
<span slot="titulek">NabÃdka sladkostÃ</span>
<li slot="položka">LÃzátko</li>
<li slot="položka">Ovocný toast</li>
</vlastni-menu>
<script>
customElements.define('vlastni-menu', class extends HTMLElement {
položky = []
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="titulek"></slot>
<ul><slot name="položka"></slot></ul>
</div>`;
// spustà se, když se obsah slotu zmÄnÃ
this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
let slot = e.target;
if (slot.name == 'položka') {
this.položky = slot.assignedElements().map(elem => elem.textContent);
alert("Položky: " + this.položky);
}
});
}
});
// aktualizujeme položky po 1 sekundÄ
setTimeout(() => {
menu.insertAdjacentHTML('beforeEnd', '<li slot="položka">KoÅ¡ÃÄky</li>')
}, 1000);
</script>
ShrnutÃ
Když element obsahuje stÃnový DOM, jeho svÄtlý DOM se zpravidla nezobrazÃ. Sloty umožÅujà zobrazit elementy ze svÄtlého DOMu na stanovených mÃstech stÃnového DOMu.
Sloty se dÄlà do dvou druhů:
- Pojmenované sloty:
<slot name="X">...</slot>â obdržà dÄti ze svÄtlého DOMu obsahujÃcÃslot="X". - Standardnà slot: prvnÃ
<slot>bez názvu (dalšà nepojmenované sloty jsou ignorovány) â obdržà dÄti ze svÄtlého DOMu nevložené do jiných slotů. - Pokud pro stejný slot existuje vÃce elementů, budou vloženy jeden za druhým.
- Obsah elementu
<slot>se použÃvá jako záloha. Zobrazà se, jestliže pro tento slot nejsou ve svÄtlém DOMu žádné dÄti.
Proces vykreslovánà elementů uvnitÅ jejich slotů se nazývá âkompoziceâ. Výsledek se nazývá âzploÅ¡tÄlý DOMâ.
Kompozice ve skuteÄnosti nepÅesunuje uzly, z pohledu JavaScriptu je DOM stále stejný.
JavaScript může pÅistupovat ke slotům pomocà tÄchto metod:
slot.assignedNodes/Elements()â vracà uzly/elementy uvnitÅslot.node.assignedSlotâ opaÄná vlastnost, vracà slot podle uzlu.
Jestliže chceme vÄdÄt, co zobrazujeme, můžeme sledovat obsah slotu pomocÃ:
- událost
slotchangeâ spustà se poprvé, kdy je slot naplnÄn, a pÅi vÅ¡ech operacÃch pÅidánÃ, odstranÄnà a nahrazenà elementu ve slotu, ale ne jeho dÄtÃ. Tento slot je vudálost.target. - MutationObserver pro vstup hloubÄji do obsahu slotu a sledovánà zmÄn uvnitÅ nÄj.
Když teÄ vÃme, jak zobrazovat elementy ze svÄtlého DOMu ve stÃnovém DOMu, podÃvejme se, jak jim správnÄ nastavit styly. Základnà pravidlo znÃ, že styly stÃnových elementů se nastavujà uvnitÅ a styly svÄtlých vnÄ, ale existujà významné výjimky.
Podrobnosti probereme v pÅÃÅ¡tà kapitole.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)