Hoppa till innehåll

HTML templates

Med metoderna från förra artikeln kan du bygga vilken struktur som helst. Frågan är om du vill det. Den här artikeln handlar om <template>-elementet, en av de fyra standarderna bakom webbkomponenter, och om varför HTML helst ska skrivas som HTML.

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

Så här kan det se ut när en liten lista med två länkar byggs med DOM-metoder:

const post = document.createElement('div')
post.classList.add('post')
const list = document.createElement('ul')
const first = document.createElement('li')
first.classList.add('active')
const firstLink = document.createElement('a')
firstLink.setAttribute('href', '#')
firstLink.textContent = 'Första länken'
first.append(firstLink)
const second = document.createElement('li')
const secondLink = document.createElement('a')
secondLink.setAttribute('href', '#')
secondLink.textContent = 'Andra länken'
second.append(secondLink)
list.append(first, second)
post.append(list)

Och så här ser samma struktur ut som HTML:

<div class="post">
<ul>
<li class="active"><a href="#">Första länken</a></li>
<li><a href="#">Andra länken</a></li>
</ul>
</div>

Den första versionen måste du läsa rad för rad för att förstå vad den bygger. Den andra ser du på några sekunder. JavaScript-koden är egentligen HTML, bara skriven på ett sätt som är svårt att läsa, svårt att ändra i och lätt att göra fel i.

Du har hela kursen delat upp kod i funktioner, klasser och moduler. Samma tänkande gäller webbläsarens tre språk, och brukar kallas separation of concerns:

Språk Ansvar
HTML struktur och innehåll
CSS presentation
JavaScript beteende

Det du har gjort hittills, med HTML i index.html, CSS i css/ och JavaScript i js/, är ett sätt att sortera: efter teknik. Det fungerar bra så länge applikationen är liten. Ett annat sätt är att sortera efter funktion, så att allt som hör till en viss del av gränssnittet ligger samlat, oavsett språk. Det är så webbkomponenter är organiserade, och det är dit du är på väg. I båda fallen gäller dock att HTML skrivs som HTML och inte byggs element för element i JavaScript.

Elementet <template> innehåller HTML som webbläsaren tolkar men inte visar. Innehållet är inert, overksamt:

  • Det renderas inte, och syns inte på sidan.
  • Skript i det körs inte, och bilder hämtas inte.
  • Det hittas inte av document.querySelector(), eftersom det inte är en del av dokumentets träd.

Innehållet ligger i stället i egenskapen content, ett DocumentFragment. Det används genom att du klonar det och sätter in kopian där du vill ha den:

<template id="user-template">
<li class="user">
<span class="name"></span>
<a class="profile">Profil</a>
</li>
</template>
const template = document.querySelector('#user-template')
const list = document.querySelector('#users')
for (const user of users) {
const item = template.content.cloneNode(true)
item.querySelector('.name').textContent = user.name
item.querySelector('.profile').href = user.url
list.append(item)
}
Flödet för en mall i tre spår. Mallen: HTML-tolken läser template-elementet, innehållet tolkas men renderas inte, och hamnar i template.content, ett DocumentFragment. Din kod: template.content.cloneNode(true) ger en kopia av hela fragmentet, som fylls i med data, till exempel med textContent. Dokumentet: list.append(kopia) flyttar kopians noder in i dokumentet, och sidan ritas om när koden har kört klart.

Lägg märke till att kopian fylls i med textContent och href efter kloningen. Mallen innehåller strukturen, koden innehåller data. Det är samma regel som för innerHTML i förra artikeln: data utifrån sätts alltid som text.

template.content.cloneNode(true) är en rad du kommer att skriva många gånger. Det är lätt att glömma .content, och då klonar du <template>-elementet i stället för dess innehåll.

const template = document.querySelector('#user-template')
const item = template.content.cloneNode()
list.append(item)

Koden ger inget fel, men inget nytt <li> dyker upp i listan. Varför?

cloneNode() utan argument gör en grund kopia, som cloneNode(false). Det som klonas är fragmentet template.content, och en grund kopia av ett fragment är ett tomt fragment. När ett tomt fragment sätts in i listan händer ingenting.

Med cloneNode(true) kopieras hela trädet i fragmentet, och <li> kommer med. Samma misstag gjordes för övrigt live i inspelningen.

Anta att du hoppar över kloningen och sätter in mallens innehåll direkt, tre gånger:

for (let i = 0; i < 3; i++) {
list.append(template.content)
}

Hur många <li> får listan?

Ett.

När ett fragment sätts in flyttas dess barn in i dokumentet, och fragmentet blir tomt. Efter första varvet är alltså template.content tomt, och de två sista anropen sätter in ett tomt fragment. Mallen är dessutom förbrukad för resten av sidans livstid.

Det är därför du alltid klonar: kopian flyttas in i dokumentet, och originalet finns kvar till nästa gång.

En <template> i index.html fungerar bra så länge den används på en enda sida. När du nästa vecka bygger webbkomponenter vill du att allt som hör till en komponent ligger i komponentens egna filer, så att komponenten går att använda var som helst. Då skapar du mallen från JavaScript i stället:

const template = document.createElement('template')
template.innerHTML = `
<li class="user">
<span class="name"></span>
<a class="profile">Profil</a>
</li>
`
const item = template.content.cloneNode(true)

Strängen är skriven med backticks, en template-sträng, så att den kan sträcka sig över flera rader. Här är innerHTML på sin plats: strängen är skriven av dig, i källkoden, utan data utifrån. Data sätts precis som förut med textContent efter kloningen. Skulle du i stället stoppa in data med ${...} i strängen är du tillbaka i problemet från förra artikeln.

Att HTML-koden nu står i en sträng känns som ett steg tillbaka, eftersom editorn visar den i samma färg som all annan text. Det finns tillägg till Visual Studio Code som färgkodar HTML i template-strängar.

Det finns ännu inget standardiserat sätt att importera en .html-fil till en JavaScript-modul. I kursen löser Vite det åt dig: med tillägget ?raw importeras filens innehåll som en vanlig sträng. Det är så den färdiga komponenten i Tiny Tunes är byggd:

import htmlText from './tiny-tune.html?raw'
const template = document.createElement('template')
template.innerHTML = htmlText

?raw är en funktion i Vite, inte i webbläsaren. Vite skriver om importen till en sträng innan koden når webbläsaren. Mer om det, och om hur komponentens filer är organiserade, kommer i artikeln En komponents filer i kursvecka 10.

Behovet av mallar är äldre än <template>. Innan elementet fanns byggdes mallmotorer som Mustache, Handlebars och Pug, och de används fortfarande mycket, framför allt på servern. Ramverk som React har egna sätt att skriva HTML i JavaScript, som JSX. De kan mer än <template>, till exempel villkor och loopar direkt i mallen.

Kursen håller sig till webbstandarderna. De fungerar i alla webbläsare utan bibliotek, och det du lär dig blir inte inaktuellt när nästa ramverk kommer. Förstår du hur <template> fungerar känner du igen idén i alla mallmotorer du stöter på.

  • Skriv HTML som HTML. Långa kedjor av createElement är svåra att läsa och underhålla.
  • Innehållet i <template> tolkas men visas inte, och ligger i template.content.
  • Klona alltid med template.content.cloneNode(true), fyll i kopian och sätt in den.
  • Data sätts med textContent efter kloningen, aldrig i mallens HTML-sträng.
  • En mall kan skapas från en sträng i JavaScript. Det är så du kommer att göra i webbkomponenterna.
CCBY