Översikt
Nu börjar du på allvar strukturera upp din kod på klienten genom att paketera den i komponenter! Information om veckans upplägg annonseras löpande i kursens Zulip-kanal.
Stödjande inspelningar
Section titled “Stödjande inspelningar”Webbkomponenter - En introduktion till “Custom Elements”
Section titled “Webbkomponenter - En introduktion till “Custom Elements””Templates som strängar i JavaScript
Section titled “Templates som strängar i JavaScript”Att bygga upp DOM-innehåll med upprepade createElement/appendChild/createTextNode-anrop blir snabbt svårläst och svårunderhållet. Ett bättre sätt är att skriva markupen som en <template> och klona innehållet:
const template = document.createElement('template')template.innerHTML = ` <div class="post"> <ul> <li class="active"><a href="#">The first link</a></li> <li><a href="#">The second link</a></li> </ul> </div>`
const clone = template.content.cloneNode(true)document.querySelector('body').appendChild(clone)När komponenter ska byggas är det bättre att skriva templaten som en JavaScript-sträng (med backticks) direkt i komponentens egen fil, istället för att ha en <template>-tagg liggande i HTML-dokumentet – då hamnar allt som hör till komponenten på ett och samma ställe. Eftersom det är utvecklaren själv som skriver strängen är innerHTML här ofarligt; risken uppstår först om data från en extern källa (användarinput, ett API-svar) skulle klistras in i samma sträng.
Custom Element – ett eget HTML-element
Section titled “Custom Element – ett eget HTML-element”Ett custom element är helt enkelt ett självdefinierat HTML-element, precis som <div> eller <img> men skräddarsytt för ett eget syfte – till exempel <my-avatar src="images/leitet.png"></my-avatar> för att presentera en avatarbild på ett specialiserat sätt. Komponentens egen fil (t.ex. js/components/my-avatar/index.js) laddas in med en <script type="module">-tagg, och den övriga applikationskoden (js/index.js) importerar komponenten direkt istället för att bara lita på att script-taggen finns i HTML – det gör beroendet synligt för den som läser koden, och skyddar mot att någon av misstag tar bort en script-tagg som “verkar oanvänd”.
Timing vid laddning
Section titled “Timing vid laddning”Moduler laddas med defer, vilket innebär att den modul som börjar laddas först också är den som exekveras först – oavsett hur lång tid själva nedladdningen tar. Laddas komponenten in före applikationens egen kod (js/index.js) finns komponenten alltså redan definierad när den egna koden körs.
Vänds ordningen (egen kod laddas före komponenten) kan ett tajmingproblem uppstå: webbläsaren stöter på <my-avatar> i HTML:en innan den vet vad elementet är, och skapar då tillfälligt ett anonymt HTML-element istället. Först när customElements.define('my-avatar', ...) senare körs uppgraderas det befintliga elementet till att vara av rätt typ – prototypreferenserna sätts om i efterhand. I praktiken hanterar webbläsaren de flesta sådana fall automatiskt, men för att undvika problem helt är det säkrast att alltid ladda in komponenten före den kod som använder den.
Namngivning av custom elements
Section titled “Namngivning av custom elements”Ett custom element måste innehålla minst ett bindestreck i namnet (<my-avatar>, aldrig <avatar>). Anledningen är att W3C garanterat aldrig kommer namnge ett inbyggt HTML-element med bindestreck, vilket förhindrar framtida namnkrockar med standarden. Delen före bindestrecket fungerar som ett namespace – ett sätt att undvika krockar med andra utvecklares komponenter också (stora företag använder ofta korta förkortningar som prefix, t.ex. tänkta fb-/sf-; ett projekt utan etablerat namn gör klokt i att välja ett något längre, tydligare prefix). Utöver bindestrecks-kravet är ett custom element som vilket HTML-element som helst – det går att välja ut och styla via CSS och nås via JavaScript på precis samma sätt.
Webbkomponenter - Filstruktur
Section titled “Webbkomponenter - Filstruktur”En komponent, en mapp
Section titled “En komponent, en mapp”En rekommenderad filstruktur samlar varje egen komponent i en egen undermapp under js/components/, till exempel js/components/my-avatar/. Där ligger komponentens egna resurser (bilder i en images-mapp, eventuella beroenden i en lib-mapp), samt två JavaScript-filer: my-avatar.js, som innehåller själva komponentens klass, och index.js, som bara importerar och exponerar den:
import './my-avatar.js'import './components/my-avatar/'Applikationens egen js/index.js importerar komponentens mapp (vilket i sin tur triggar index.js inuti mappen). Mönstret med en tunn index.js som pekar vidare till den faktiska implementationsfilen är detsamma oavsett hur många lager av mappar en modul är nästlad i.
import.meta.url – relativa sökvägar till egna resurser
Section titled “import.meta.url – relativa sökvägar till egna resurser”En komponent behöver ofta referera till sina egna resurser, till exempel en standardbild, utan att hårdkoda en sökväg som slutar bero på var i applikationen komponenten råkar vara monterad. import.meta.url ger den körande modulens egen URL, vilket gör att en sökväg kan räknas ut relativt komponentens egen fil istället för relativt sidans rot:
let pathToModule = import.meta.url// t.ex. http://localhost:3000/js/components/my-avatar/index.js
let path = new URL('./images/default.png', pathToModule)// http://localhost:3000/js/components/my-avatar/images/default.png
// ... <img id="avatar" src="${path}" /> ...Fördelen är att komponentmappen blir självförsörjande och flyttbar – flyttas hela my-avatar-mappen till en annan plats i projektet (eller till ett helt annat projekt) fortsätter sökvägarna till dess egna bilder att fungera, eftersom de aldrig var beroende av var komponenten monterades från början.
Använda npm-moduler i en komponent
Section titled “Använda npm-moduler i en komponent”Precis som i vilken annan del av applikationen som helst kan en komponent ha beroenden till paket installerade via npm:
$ npm install md5import md5 from 'md5'Byggverktyget tar hand om att paketera (bundla) node_modules-beroendet tillsammans med resten av koden – något som inte kräver någon särskild hantering bara för att koden råkar ligga i en komponentmapp.
Webbkomponenter - Skapa Custom Elements
Section titled “Webbkomponenter - Skapa Custom Elements”Ett custom element utan Shadow DOM räcker inte
Section titled “Ett custom element utan Shadow DOM räcker inte”Ett första försök att skapa en komponent kan se ut så här:
customElements.define('my-avatar', class extends HTMLElement { constructor () { super() this.innerHTML = ` <div> <img id="avatar" src="js/components/my-avatar/images/default.png" /> </div>` } })Det här fungerar faktiskt inte alls – att sätta innerHTML direkt i konstruktorn kastar ett fel (Failed to construct 'CustomElement': The result must not have children). Ett custom element får inte skapa barn-innehåll i konstruktorn på det sättet. Lösningen är Shadow DOM.
Shadow DOM
Section titled “Shadow DOM”Inbyggda element som <video> har länge haft ett eget, inkapslat DOM-träd som inte syns i den vanliga DOM-strukturen – det är därför videospelarens kontroller inte går att komma åt via vanlig querySelector på sidan. Shadow DOM ger samma möjlighet till egna, kapslade komponenter: en shadowRoot kopplas till elementet (kallat shadow host), och allt innehåll som hör till komponentens interna implementation läggs där istället för direkt i det vanliga DOM-trädet.
const template = document.createElement('template')template.innerHTML = ` <style> img { clip-path: circle(50%); } </style> <div> <img id="avatar" src="" /> </div>`
customElements.define('my-avatar', class extends HTMLElement { #image
constructor () { super() // Attach a shadow DOM tree to this element and // append the template to the shadow root. this.attachShadow({ mode: 'open' }) .appendChild(template.content.cloneNode(true))
// Get a reference to the image-element. this.#image = this.shadowRoot.querySelector('#avatar') } })attachShadow({ mode: 'open' }) skapar shadow-roten och gör den läsbar utifrån (element.shadowRoot); en klonad template-nod läggs till där. CSS skriven inuti templaten (eller i en <style>-tagg i shadow-roten) gäller bara inuti komponenten – den läcker varken in eller ut.
Lifecycle callbacks
Section titled “Lifecycle callbacks”Ett custom element kan implementera ett antal metoder som webbläsaren själv anropar vid olika tillfällen:
-
constructor– anropas när elementet skapas eller uppgraderas. Här hör initiering av state, uppsättning av shadow DOM och event listeners hemma. -
attributeChangedCallback (name, oldValue, newValue)– anropas när ett observerat attributs värde ändras (läggs till, tas bort eller byts), samt när elementet skapas/uppgraderas. Vilka attribut som ska observeras listas explicit i en statisk getter:static get observedAttributes () {return ['src']}attributeChangedCallback (name, oldValue, newValue) {if (name === 'src' && newValue !== oldValue) {this.#image.setAttribute('src', newValue)}} -
connectedCallback– anropas när elementet läggs till i DOM:en (eller ett nytt DOM-träd). -
disconnectedCallback– anropas när elementet tas bort från DOM:en. Ett naturligt ställe att städa upp event listeners och liknande. -
adoptedCallback– anropas om elementet flyttas till ett annat dokument. Sällan aktuellt.
En <slot> i templaten är en platshållare där innehåll som skrivs mellan komponentens öppnings- och stängningstagg i vanlig HTML får plats:
<my-gravatar email="johan.leitet@lnu.se"> <p><strong>Adding</strong> some content to our element.</p></my-gravatar>template.innerHTML = ` <div> <img class="avatar" id="gravatar" src="" /> <slot class="info"></slot> </div>`Har <slot> ett name-attribut (en named slot) kan flera olika slots fyllas med olika innehåll samtidigt, genom att matcha namnet mot ett slot-attribut på det inskickade innehållet:
template.innerHTML = ` <div> <h3><slot name="name">Unknown</slot></h3> <p><slot name="info"></slot></p> </div>`<my-gravatar email="johan.leitet@lnu.se"> <span slot="name">Johan Leitet</span> <span slot="info">This is some text used to explain something</span></my-gravatar>Text mellan slot-taggarna i templaten (t.ex. Unknown ovan) fungerar som standardinnehåll om inget matchande innehåll skickas in.
CSS i komponenter: :host, ::part() och ::slotted()
Section titled “CSS i komponenter: :host, ::part() och ::slotted()”Tre specialfall behövs för att styla över shadow-gränsen:
-
:host– en pseudoklass som, använd inuti komponentens egen CSS, refererar till shadow host-elementet självt (t.ex.<my-gravatar>). -
::part()– en pseudoelement som, använt i CSS utanför komponenten, kommer åt ett internt element som explicit exponerats med ettpart-attribut:css/styles.css my-gravatar::part(avatar) {clip-path: circle(50%);}js/components/my-avatar/index.js // <img part="avatar" ... /> -
::slotted()– ett pseudoelement som, använt inuti komponentens egen CSS, kommer åt innehåll som skickats in via en slot:::slotted(*) {max-width: 100%;max-height: 100%;}
Best practices
Section titled “Best practices”Några tumregler för egna komponenter: kasta helst inte fel om det går att undvika, efterlikna hur inbyggda element beter sig (t.ex. src snarare än ett eget påhittat source-attributnamn), inkludera samtliga beroenden i komponentens egen mapp, och dokumentera komponenten – vilka attribut, metoder, events och delar (part) den exponerar, samt ett användningsexempel. En README.md i komponentens egen mapp är ett naturligt ställe för den dokumentationen.
Läsanvisningar
Section titled “Läsanvisningar”Under läsanvisningar hittar du veckans kapitel som ska läsas i kurslitteraturen. Planera upp och dela upp läsningen under veckan.
Övningar
Section titled “Övningar”Under Övningsuppgifter hittar du övningsuppgifter som det är lämpligt att du arbetar med under veckan.
Handledning
Section titled “Handledning”Skicka gärna in frågor på förhand via formuläret som du når här:
Examinationsuppgift A3
Section titled “Examinationsuppgift A3”Inlämning av examinationsuppgift A3 och därtill hörande muntlig examination!
Studiefrågor
Section titled “Studiefrågor”Här är ett antal frågor som du kan försöka besvara och diskutera tillsammans med din AI-assistent. Tveka inte att lyfta någon av frågorna till handledningen för att fortsätta diskussionen med lärare och studiekamrater.
- Förklara kortfattat grunden i webbkomponenter i JavaScript i webbläsaren.
- Hur ser det ut i HTML-koden när man använder en webbkomponent?
- Måste, eller bör jag, använda shadow dom?
- Måste, eller bör jag, använda mig av HTML templates?
- Finns det förslag till kodstruktur och uppdelning av filer, när jag skapar en komponent?
- Är webbkomponenter någon form av best practice som används ofta ute i industrin?
Här är en utskrift från hur det såg ut när jag diskuterade ovan frågor med min AI-assistent Ajj.
Här följer tips till hur du kan prioritera bland kursmaterialet för att komma igång med veckan.
- Missa inte att träna på den teoretiska delen inför examinationen.
- När du har examinationen, se till att du har koll på alla delar i din kod. Om examinatorn frågar dig “var gör du detta” så bör du kunna hitta den delen på ett smidigt sätt och samtidigt översiktligt förklara vad koden gör i sammanhanget. Dykar man ner i detaljer så bör du kunna förklara även detta. Det är ju din kod som du själv har skrivit.
- Genomför övningsuppgiften “Tiny Tunes”.
