Hoppa till innehåll

Bra komponenter

Du kan nu bygga en webbkomponent: en klass, ett shadow DOM, attribut och metoder in, händelser ut. Den här veckan handlar om vad som händer när komponenterna blir fler och ska fungera tillsammans. Det börjar med frågan om vad som gör en komponent bra, för det är bra komponenter som går att sätta ihop.

Artikeln bygger på custom elements från kursvecka 10 och händelser i webbkomponenter från kursvecka 11. Exemplen kommer från exemplet Att göra, med komponenterna todo-list och todo-item, och från veckans övningsuppgift, YouTube Player.

Tidsåtgång: Ungefär 15 minuter.

En bra komponent gör en sak. todo-item visar en uppgift och en kryssruta, och vet om uppgiften är klar. Den vet inte hur många uppgifter som finns, eller att den ligger i en lista.

Det finns knappast någon undre gräns för hur liten en komponent kan vara. Det finns däremot en övre: en hel applikation är inte en komponent, utan något som är byggt av komponenter. En applikation som en musiktjänst kan bestå av ett sjuttiotal olika komponenter, som återanvänds på många ställen.

Den vanliga fällan är för få komponenter, inte för många. Det är bekvämt att lägga till ännu en funktion i den komponent du redan har, på samma sätt som det är bekvämt att kopiera kod i stället för att bryta ut en funktion. Ett bra tecken på att något borde brytas ut är att du har svårt att beskriva vad komponenten gör utan att säga “och”.

Samtidigt behöver du inte börja där. En stor komponent som fungerar går att dela upp senare. Fem små som inte fungerar hjälper ingen. Gör det enkelt först, och dela upp när du ser var gränserna går.

Den bästa förebilden för en komponent är de element som redan finns. Den som har skrivit HTML vet hur de fungerar, och en komponent som fungerar likadant behöver ingen bruksanvisning.

Titta på <video>:

Riktning <video> Din komponent
In attribut: src, controls, loop attribut, för enkla inställningar
In metoder: play(), pause() metoder, för handlingar och sammansatt data
Ut händelser: play, ended, error egna händelser, med data i detail

Det är samma tre kanaler som du har använt sedan kursvecka 10: data ned via attribut och metoder, data upp via händelser. Attribut är förstahandsvalet. Det mesta som en komponent behöver veta går att beskriva som ett attribut, precis som src på en <img>.

En komponent returnerar alltså inget till den som använder den. Precis som <video> inte svarar på play() med “nu är jag klar”, utan utlöser ended när filmen är slut, berättar din komponent med en händelse när något har hänt.

Ett attributs värde är alltid en sträng, oavsett hur det ser ut i HTML-koden eller vad du skickar till setAttribute():

player.setAttribute('start', 30)
player.getAttribute('start') // '30'

Behöver komponenten ett tal gör den om strängen själv, och väljer ett standardvärde om attributet saknas:

const start = Number(this.getAttribute('start') ?? 0)

Det betyder också att attribut passar för enkla värden: text, tal och ja/nej. En array eller ett objekt hör inte hemma i ett attribut. Att göra om det till JSON med JSON.stringify() och tolka det igen inuti komponenten fungerar tekniskt, och du hittar förslag på det på nätet, men det är ett fulhack. Sammansatt data skickas med en metod:

bookList.showBooks(books)

Ja/nej-inställningar fungerar som de inbyggda booleska attributen, <video loop> och <input disabled>: finns attributet är det sant, saknas det är det falskt. Värdet spelar ingen roll. todo-item använder attributet done på det sättet:

<todo-item done>Do the dishes</todo-item>

Komponenten läser det med hasAttribute():

this.#checkbox.checked = this.hasAttribute('done')

Skriv alltså done, inte done="true".

youtube-scene ska stänga av loopningen på en youtube-player, och gör så här:

player.setAttribute('loop', false)

Spelaren läser attributet med this.hasAttribute('loop'). Loopar videon? Hur borde youtube-scene ha skrivit?

Ja, videon loopar. setAttribute() gör om false till strängen 'false', och attributet loop="false" finns. hasAttribute('loop') ger true.

Ett booleskt attribut slås av genom att det tas bort. toggleAttribute() gör båda delarna: med true som andra argument läggs attributet till utan värde, med false tas det bort:

player.toggleAttribute('loop', false)

Det är så youtube-scene i övningen sätter autoplay, controls, loop och related.

En komponent bryr sig bara om det som finns inuti den. Den sträcker sig inte ut i dokumentet för att hämta eller ändra något där:

// Avoid: searches the whole page, not just this component
const items = document.querySelectorAll('todo-item')

Den raden fungerar så länge det finns en enda lista på sidan. Lägg till en till, och listan räknar även den andra listans uppgifter. Komponenten har dessutom fått ett dolt beroende: den fungerar bara på en sida som ser ut på ett visst sätt.

Sök i stället från komponenten själv, this eller this.shadowRoot, beroende på var det du letar efter finns. Vilket som gäller ser du i nästa artikel, Komponenter i komponenter.

Ett enkelt sätt att granska sin egen kod: sök efter document. i komponentens filer. Varje träff är värd en fråga.

  • document.createElement() är i sin ordning. Det finns ingen this.createElement(), och elementet hamnar inte i dokumentet förrän du lägger in det. Lägg in det i komponentens eget träd.
  • document.addEventListener() kan också vara i sin ordning. En komponent som reagerar på kortkommandon måste lyssna på document, eftersom tangenttryckningarna går dit. Den läser händelsen, men ändrar inget utanför sig själv, och tar bort lyssnaren i disconnectedCallback().
  • document.querySelector() och liknande, som hämtar eller ändrar andras element, är nästan alltid fel i en komponent.

Det enda vedertagna undantaget är <head>. En komponent som är beroende av ett externt skript kan behöva lägga till ett <script>-element där. Pröva först om en dynamisk import() räcker, så att komponenten inte behöver röra dokumentet alls.

index.js, som sätter ihop applikationen, är en annan sak. Den ska känna till sidan, och där är document.querySelector() rätt verktyg.

En komponent som går sönder ska inte ta applikationen med sig. Men det betyder inte att felen ska tystas. Var felet ska tas om hand beror på vem som kan göra något åt det.

Fel i något komponenten gör på eget initiativ, i en lyssnare eller en timer, måste komponenten ta hand om själv. Ingen annan anropade koden, så ingen annan kan fånga felet. Komponenten visar ett begripligt meddelande, eller berättar för sin förälder att något gick fel med en händelse, på samma sätt som en <img> utlöser error när bilden inte kan laddas:

this.dispatchEvent(new CustomEvent('error', {
detail: { message: 'The video could not be loaded.' }
}))

Fel i en metod som föräldern anropar kan i stället få gå vidare. Föräldern anropade, föräldern kan fånga. Det gäller i synnerhet nätverksanrop: den komponent som pratar med servern vet bäst vad ett misslyckande betyder för resten av applikationen.

En komponent laddar en bok när användaren klickar på en knapp:

connectedCallback() {
this.#abortController = new AbortController()
this.#button.addEventListener('click', () => this.#load(), {
signal: this.#abortController.signal
})
}
async #load() {
const response = await fetch(this.getAttribute('src'))
if (!response.ok) {
throw new Error(`Could not load the book (status ${response.status}).`)
}
this.#showBook(await response.json())
}

Den som använder komponenten har lagt till en try/catch runt raden där komponenten läggs in på sidan. Servern svarar 404. Var hamnar felet?

Ingenstans, eller snarare i konsolen som Uncaught (in promise) Error. try/catch runt append() fångar bara fel som kastas medan append() körs. Klicket kommer långt senare, och då anropas #load() av webbläsaren, från lyssnaren. Promiset som #load() returnerar tar ingen emot.

Användaren märker ingenting; knappen verkar bara inte göra något. #load() måste fånga felet själv och visa det, eller skicka det vidare som en händelse. Det är samma regel som i Nätverksanrop med fetch: en asynkron metod som anropas från en lyssnare tar hand om sina egna fel.

  • En bra komponent är liten och gör en sak. Den vanliga fällan är för få komponenter, men börja enkelt och dela upp när gränserna syns.
  • Härma de inbyggda elementen: attribut och metoder in, händelser ut.
  • Attribut är strängar. Gör om dem själv, och skicka sammansatt data med en metod, aldrig som JSON i ett attribut.
  • Booleska attribut betyder sant när de finns. Läs dem med hasAttribute() och sätt dem med toggleAttribute().
  • Sök aldrig i hela dokumentet från en komponent. Sök från this eller this.shadowRoot.
  • Fel som uppstår i en lyssnare tar komponenten hand om själv. Fel i en metod som föräldern anropar kan gå vidare till föräldern.
CCBY