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.
Liten, med ett ansvar
Section titled “Liten, med ett ansvar”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.
Härma de inbyggda elementen
Section titled “Härma de inbyggda elementen”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.
Attribut är strängar
Section titled “Attribut är strängar”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)Booleska attribut
Section titled “Booleska attribut”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".
Fråga: är videon loopad?
Section titled “Fråga: är videon loopad?”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.
Håll dig inom komponenten
Section titled “Håll dig inom komponenten”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 componentconst 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 ingenthis.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 idisconnectedCallback().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.
När något går fel
Section titled “När något går fel”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.
Fråga: vem fångar felet?
Section titled “Fråga: vem fångar felet?”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.
Att ta med sig
Section titled “Att ta med sig”- 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 medtoggleAttribute(). - Sök aldrig i hela dokumentet från en komponent. Sök från
thisellerthis.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.
Vidare läsning
Section titled “Vidare läsning”- Custom element best practices på web.dev, en längre checklista. Den rekommenderar egenskaper med getters och setters för data in, vilket kursen avstår från av skälen i Custom elements.
- Element.toggleAttribute() och Boolean attributes på MDN.
- Komponenternas
README.mdi exemplet Att göra och i övningsuppgiften YouTube Player, och avsnittet om booleska attribut i övningensARCHITECTURE.md.