Komponenter i komponenter
HTML bygger på att små element sätts ihop till större. En <ul> innehåller <li>, ett <form>
innehåller <input> och <button>. Komponenter sätts ihop på samma sätt, och det finns två sätt att
göra det. Den här artikeln går igenom båda, och bygger på
shadow DOM och slots från kursvecka 10 och den föregående
artikeln, Bra komponenter.
Exemplen kommer från exemplet Att göra,
där todo-list innehåller todo-item, och från övningsuppgiften YouTube Player,
där youtube-scene innehåller youtube-control och youtube-player.
Tidsåtgång: Ungefär 15 minuter.
Två sätt att bygga ihop
Section titled “Två sätt att bygga ihop”Barnen kan ligga i två olika träd:
- I light DOM, alltså som vanliga barn till komponenten i dokumentets träd. Den som använder
komponenten skriver dem, och komponenten visar dem med ett
<slot>. Så fungerartodo-list, och så fungerar<ul>. - I shadow DOM. Komponenten skapar barnen själv, i sin egen mall, och ingen utanför ser dem.
Så fungerar
youtube-scene, och så fungerar<video controls>: knapparna och reglagen finns, men du har inte skrivit dem och kommer inte åt dem.
Vem bestämmer över barnen?
Section titled “Vem bestämmer över barnen?”Valet handlar om vem som ska bestämma över barnen.
Med barn i light DOM bestämmer den som använder komponenten. Det är lätt att lägga till, ta bort och ändra en uppgift i listan, både i HTML-koden och med JavaScript, och det kräver inget annat än det alla redan kan:
<todo-list> <todo-item>Buy milk</todo-item> <todo-item done>Do the dishes</todo-item></todo-list>Med barn i shadow DOM bestämmer komponenten. Den som använder youtube-scene skriver bara
<youtube-scene></youtube-scene> och behöver inte veta att det finns ett formulär och en spelare
inuti. Komponenten kan byta ut sina delar utan att någon utanför märker det.
Fråga dig om barnen är innehåll eller delar. Uppgifterna i en att göra-lista är innehåll; de
är olika varje gång listan används. Formuläret och spelaren i youtube-scene är delar; de är alltid
desamma. Innehåll hör hemma i light DOM, delar i shadow DOM.
En komponent kan göra båda. todo-item har sin kryssruta i shadow DOM, som en del, och tar emot
texten som innehåll genom ett <slot>.
Att hitta barnen
Section titled “Att hitta barnen”Var barnen finns avgör hur komponenten hittar dem. Barn i light DOM är vanliga barn till
komponenten, och nås från this:
const items = this.querySelectorAll('todo-item')Barn i shadow DOM nås från this.shadowRoot:
this.#player = this.shadowRoot.querySelector('youtube-player')I inget av fallen söker komponenten i document, av skälen i
föregående artikel.
Fråga: vad hittar todo-list?
Section titled “Fråga: vad hittar todo-list?”todo-list och todo-item har var sitt shadow DOM med var sitt <slot>, och todo-item har en
kryssruta i sitt. Vad ger de här anropen inuti todo-list, med listan ovan?
this.querySelectorAll('todo-item').lengththis.shadowRoot.querySelectorAll('todo-item').lengththis.querySelectorAll('input').length2, 0 och 0.
Uppgifterna är barn till todo-list i dokumentets träd. Att de visas i listans <slot> flyttar
dem inte in i listans shadow-träd, så this.shadowRoot hittar dem inte.
Kryssrutorna finns i varje todo-items shadow-träd. Varken this.querySelectorAll() eller
this.shadowRoot.querySelectorAll() letar sig in i ett annat elements shadow DOM. Det är
inkapslingen som gör att todo-list inte kan börja bero på hur todo-item är byggd inuti.
Behöver komponenten bara de element som just nu visas i en viss slot finns assignedElements() på
slot-elementet. Det är användbart när en komponent har flera namngivna slots.
Barn i light DOM
Section titled “Barn i light DOM”När barnen kommer och går
Section titled “När barnen kommer och går”todo-list visar hur många uppgifter som är klara. Räknar den bara i connectedCallback() blir
siffran fel så fort någon lägger till en uppgift, eftersom listan inte vet om det.
Ett <slot> utlöser händelsen slotchange när de element som visas i den ändras, alltså när barn
läggs till eller tas bort:
connectedCallback() { this.#abortController = new AbortController() const signal = this.#abortController.signal
const slot = this.shadowRoot.querySelector('slot') slot.addEventListener('slotchange', () => this.#update(), { signal }) this.addEventListener('statechanged', () => this.#update(), { signal })
this.#update()}
#update() { const items = [...this.querySelectorAll('todo-item')]
this.#total.textContent = items.length this.#completed.textContent = items.filter((item) => item.hasAttribute('done')).length}slotchange utlöses inte när ett barn ändras, bara när det kommer eller går. Att en uppgift bockas
av berättar todo-item själv, med händelsen statechanged, och det är den andra lyssnaren.
Händelser från barnen
Section titled “Händelser från barnen”todo-item utlöser statechanged med bubbles: true:
#onCheckboxChange() { this.toggleAttribute('done', this.#checkbox.checked)
this.dispatchEvent(new CustomEvent('statechanged', { detail: { done: this.#checkbox.checked }, bubbles: true }))}Det är fallet som kursens händelsekonvention
tar upp: bubbles behövs när föräldern inte lyssnar på barnet. todo-list vet inte vilka uppgifter
den kommer att få, så den har en enda lyssnare på sig själv i stället för en på varje uppgift. Det
kallas händelsedelegering. Händelsen bubblar från uppgiften till listan som vilken händelse som
helst, eftersom uppgiften är listans vanliga barn. composed behövs inte; händelsen utlöses på
todo-item och lämnar aldrig något shadow-träd.
Fråga: når händelsen in i listan?
Section titled “Fråga: når händelsen in i listan?”Skulle todo-list kunna lyssna på statechanged på <div>-elementet runt sin <slot>, inuti sitt
eget shadow DOM, i stället för på this?
this.shadowRoot.querySelector('div').addEventListener('statechanged', ...)Ja. Ett element som visas i en slot har slotten som förälder när en händelse ska bubbla. Händelsen
går från todo-item till <slot>, vidare till <div> och listans shadow-root, och därefter ut till
todo-list och upp genom dokumentet.
Inspelningen från 2021 gissar fel här, men slutsatsen står sig: lyssnaren på this är det enkla
valet, och den fungerar oavsett hur listans mall ser ut.
Att händelsen fortsätter upp genom dokumentet har en följd. Även kod utanför listan kan lyssna på
den, och det utnyttjar exemplets index.js, som du ser i nästa artikel. Det har också en baksida:
en bubblande händelse når alla förfäder, så den behöver ett namn som inte krockar med andras. Ett
allmänt namn som change vore en dålig idé.
Fråga: uppdateras räknaren?
Section titled “Fråga: uppdateras räknaren?”En knapp på sidan ska bocka av alla uppgifter, och index.js gör så här:
for (const item of list.querySelectorAll('todo-item')) { item.toggleAttribute('done', true)}Kryssrutorna blir ikryssade, eftersom todo-item lyssnar på sitt attribut. Visar listan rätt antal
klara uppgifter?
Nej. Inga barn har kommit eller gått, så ingen slotchange. Och todo-item utlöser statechanged
bara när användaren klickar, inte när attributet ändras utifrån.
Det är medvetet, och det härmar de inbyggda elementen: sätter du checked på en kryssruta med
JavaScript utlöses ingen change. Den som ändrar ett attribut vet redan om det och behöver ingen
händelse. Händelser berättar om sådant som föräldern inte själv har orsakat.
Ska listan hålla räkningen även då finns två vägar. Antingen säger den som ändrade till listan att
räkna om, med en metod. Eller så bevakar listan sina barns done-attribut med en
MutationObserver. Den andra gör listan självgående, men kräver att den känner till ännu ett
attribut hos sina barn.
Barn i shadow DOM
Section titled “Barn i shadow DOM”youtube-scene skapar sina barn i sin egen mall:
<main> <youtube-player></youtube-player> <youtube-control></youtube-control></main>När mallen klonas in i shadow-roten skapas båda elementen, och konstruktorn kan hämta dem med en gång:
constructor() { super() this.attachShadow({ mode: 'open' }) this.shadowRoot.adoptedStyleSheets = [stylesheet] this.shadowRoot.append(template.content.cloneNode(true))
this.#control = this.shadowRoot.querySelector('youtube-control') this.#player = this.shadowRoot.querySelector('youtube-player')}Föräldern har skapat barnen och har referenser till dem. Den lyssnar direkt på dem och skickar data
till dem med attribut och metoder. Barnens händelser behöver varken bubbles eller composed.
I mallen eller med createElement()?
Section titled “I mallen eller med createElement()?”Står barnen i mallen finns de alltid, ett av varje. Det passar delar, som formuläret och spelaren.
Det passar också förslaget på arkitektur i
examinationsuppgift B2, där det alltid finns exakt en
quiz-question och en countdown-timer.
Varierar antalet, som rader i en sökträfflista, skapas barnen med document.createElement() och
läggs in i komponentens eget träd:
#showBooks(books) { const items = books.map((book) => { const item = document.createElement('book-item') item.setAttribute('book-id', book.id) item.textContent = book.title return item })
this.#list.replaceChildren(...items)}replaceChildren() tar bort de gamla raderna och lägger in de nya i ett svep.
Registrera barnen
Section titled “Registrera barnen”En komponent med barn i shadow DOM fungerar bara om barnens taggar är registrerade. Därför laddar
föräldern barnen. Importen ligger i förälderns index.js, inte i klassfilen, så att klassen fortsatt
går att importera utan att någon tagg registreras:
import '../youtube-control/index.js'import '../youtube-player/index.js'import { YoutubeScene } from './youtube-scene.js'
if (!customElements.get('youtube-scene')) { customElements.define('youtube-scene', YoutubeScene)}Applikationen behöver då bara ladda youtube-scene, och får de andra två på köpet. Varför klassen
och registreringen ligger i olika filer läste du i
En komponents filer.
Att ta med sig
Section titled “Att ta med sig”- Barn i light DOM är innehåll som den som använder komponenten bestämmer över. De visas med
<slot>och nås medthis.querySelector(). - Barn i shadow DOM är delar som komponenten bestämmer över. De skapas i mallen eller med
document.createElement()och nås medthis.shadowRoot.querySelector(). slotchangeberättar att barn har kommit eller gått, inte att de har ändrats.- Barn i light DOM berättar om ändringar med en händelse med
bubbles: true, och föräldern lyssnar på sig själv. Barn i shadow DOM lyssnar föräldern på direkt. - Händelser berättar om sådant föräldern inte själv har orsakat, precis som
changepå en kryssruta. - En förälder som skapar barn i shadow DOM importerar barnens
index.jsfrån sin egenindex.js.
Vidare läsning
Section titled “Vidare läsning”- Exemplet
Klickräknaren – komposition,
där
<click-counter>från kursvecka 11 placeras i en<score-board>, först i light DOM och sedan i shadow DOM, så att du kan jämföra de två sätten sida vid sida. - Using templates and slots, HTMLSlotElement: slotchange event och HTMLSlotElement.assignedElements() på MDN.
- MutationObserver på MDN, för den som vill låta en förälder bevaka sina barns attribut.
- Exemplet Att göra, med barn i light DOM, och övningsuppgiften YouTube Player, med barn i shadow DOM.