Hoppa till innehåll

Att ändra i DOM:en

I förra artikeln lärde du dig hitta i DOM-trädet. Nu är det dags att ändra i det: skapa nya element, flytta och ta bort befintliga, sätta text, attribut och klasser. Det är precis det du gör i veckans övningsuppgift Tiny Tunes.

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

Några korta frågor finns insprängda i texten.

Nya noder skapas med metoder på document:

const paragraph = document.createElement('p') // an empty <p></p>
const text = document.createTextNode('Hej världen!') // a text node
paragraph.appendChild(text) // <p>Hej världen!</p>

Ett nytt element syns inte förrän du har satt in det i dokumentet. Fram till dess är det ett fristående objekt som bara din kod känner till. Det är ofta praktiskt: bygg klart elementet, med text, attribut och klasser, och sätt in det sist.

Den klassiska metoden är appendChild(), som lägger till en nod sist bland ett elements barn. insertBefore(nyNod, referensnod) sätter in en nod före ett av elementets barn. Båda finns i otaliga exempel och fungerar utmärkt.

Sedan några år finns också en uppsättning modernare metoder som är enklare att använda. De tar en eller flera noder, och strängar blir automatiskt textnoder:

<!-- target.before(node) -->
<ul id="target">
<!-- target.prepend(node) -->
<li>Befintligt barn</li>
<!-- target.append(node) -->
</ul>
<!-- target.after(node) -->
const list = document.querySelector('#target')
const item = document.createElement('li')
item.append('Ny punkt') // the string becomes a text node
list.append(item) // last in the list
list.before('Rubrik: ') // as a sibling before the list

Inspelningen nämner att det inte finns någon insertAfter(), så att man får kombinera insertBefore() med nextSibling. Det stämmer fortfarande, men i dag skriver du bara referens.after(nyNod).

En nod kan bara finnas på ett ställe i trädet. Sätter du in en nod som redan finns i dokumentet flyttas den; du behöver inte ta bort den först.

<ol id="queue">
<li>Ada</li>
<li>Grace</li>
<li>Linus</li>
</ol>
const queue = document.querySelector('#queue')
queue.append(queue.firstElementChild)

Ada flyttas sist: listan blir Grace, Linus, Ada.

append() får en nod som redan finns i listan, så den tas bort från sin gamla plats och sätts in på den nya. Det blir ingen kopia. Vill du ha en kopia får du klona noden först (se nedan).

Vill du Modern metod Klassisk metod
ta bort en nod node.remove() parent.removeChild(node)
ersätta en nod oldNode.replaceWith(newNode) parent.replaceChild(newNode, oldNode)
tömma ett element element.replaceChildren() loopa med removeChild()

Att tömma ett element är vanligt, till exempel innan du ritar om en lista. Ta bort alla barnnoder, inte bara alla element; annars blir textnoderna med mellanrum kvar. replaceChildren() utan argument gör det i ett anrop. Med argument ersätter den det gamla innehållet med det nya.

cloneNode() gör en kopia av en nod. Argumentet avgör hur mycket som följer med:

  • cloneNode(true) kopierar noden och hela trädet under den, alla barn, barnbarn och textnoder.
  • cloneNode(false), eller utan argument, kopierar bara noden själv. Ett klonat <ul> blir tomt.

Två saker följer inte med eller bör ändras. Händelselyssnare som lagts till med addEventListener() (kursvecka 11) kopieras inte. Och har originalet ett id får kopian samma, så byt eller ta bort det innan du sätter in kopian, eftersom id ska vara unika.

cloneNode(true) är också kärnan i hur HTML templates används, som du läser om i nästa artikel.

Ska du skapa många element, till exempel en lista med tio punkter, kan du bygga dem i ett DocumentFragment. Ett fragment är en lätt behållare som inte är en del av dokumentet. När du sätter in fragmentet flyttas dess barn in i dokumentet, och fragmentet självt blir tomt.

const fragment = document.createDocumentFragment()
for (const name of ['Ada', 'Grace', 'Linus']) {
const item = document.createElement('li')
item.textContent = name
fragment.append(item)
}
list.append(fragment) // the document changes once, not three times

Webbläsaren ritar ändå inte om sidan medan din kod körs, eftersom koden och renderingen delar huvudtråden (se Webbläsaren som körmiljö). Fragmentets fördel är i första hand tydlig kod: bygg klart vid sidan av, sätt in en gång. Har du elementen i en array fungerar också list.append(...items).

Att skapa en textnod och sätta in den blir fort omständligt. Två egenskaper gör det enklare:

element.textContent = 'Ny text' // replaces all content with a text node
element.innerHTML = '<p>Ny text</p>' // parses the string as HTML

textContent skapar alltid bara text. Innehåller strängen < och > visas de som tecken på sidan.

innerHTML skickar strängen genom HTML-tolken, samma tolk som läser in sidan, och ersätter elementets innehåll med de element och textnoder som blir resultatet. Det är bekvämt, men farligt om någon del av strängen kommer utifrån.

Anta att name kommer från ett formulärfält som besökaren har fyllt i:

const name = '<img src="x" onerror="alert(\'Hackad!\')">'
first.textContent = `Hej ${name}!`
second.innerHTML = `Hej ${name}!`

Vad händer i first och i second?

I first visas hela strängen som text, inklusive <img src=...>. Ingenting körs.

I second tolkas strängen som HTML och ett <img>-element skapas. Bilden x finns inte, så onerror utlöses och JavaScript-koden körs: en ruta med texten “Hackad!” visas.

Det här kallas XSS, cross-site scripting. I stället för en harmlös alert() kan angriparens kod läsa vad användaren skriver, skicka iväg data eller göra saker i användarens namn. Har strängen sparats på en server drabbas dessutom alla som visar den.

Inspelningen säger att <script>-taggar i en sträng körs när de sätts in med innerHTML. Det gör de faktiskt inte, eftersom webbläsaren medvetet hoppar över skript som kommer den vägen. Men som frågan visar finns andra vägar, som händelseattribut, så slutsatsen står sig:

  • Använd textContent för all text, och alltid för data som kommer från användaren, ett API eller någon annan källa du inte kontrollerar.
  • Använd innerHTML bara med strängar du själv har skrivit i källkoden, utan insprängd data utifrån. Det är så du kommer att använda den när du skapar mallar i nästa artikel.

Du kan också stöta på innerText. Den liknar textContent, men tar hänsyn till CSS, till exempel dolda element. Den är långsammare och ger sällan något du behöver; använd textContent.

HTML-attribut läser och ändrar du med fyra metoder:

link.getAttribute('href') // reads the value, or null
link.setAttribute('href', 'https://lnu.se/') // sets or overwrites
link.hasAttribute('target') // true or false
link.removeAttribute('target') // removes the attribute

De flesta standardattribut finns också som egenskaper direkt på elementet: link.href, image.src, image.alt, element.id. Inspelningen beskriver egenskaperna som en Microsoft-genväg utanför standarden, men de är standardiserade och lika korrekta att använda. Skillnaden är att egenskapen ger det tolkade värdet, medan attributet ger det som står i HTML-koden:

// <a href="/kurser">...</a> on the page https://coursepress.lnu.se/
link.getAttribute('href') // '/kurser'
link.href // 'https://coursepress.lnu.se/kurser'
// <input value="Ada">, and the user has typed "Grace"
input.getAttribute('value') // 'Ada', the initial value
input.value // 'Grace', the current value

Ett exempel på nyttan: sätter du src på ett nytt <img>-element börjar webbläsaren hämta bilden direkt, redan innan elementet har satts in i dokumentet. Glöm inte en alt-text som beskriver bilden, eller alt="" om bilden bara är dekoration.

button.setAttribute('disabled', 'false')

Går det att klicka på knappen efter raden ovan?

Nej, knappen är avstängd.

disabled är ett så kallat booleskt attribut. Det är förekomsten av attributet som räknas, inte dess värde. Värdet 'false' är bara en sträng, och strängen gör ingen skillnad. Använd egenskapen i stället, som tar ett riktigt booleskt värde:

button.disabled = false

eller ta bort attributet med button.removeAttribute('disabled').

Egna data på ett element lägger du i attribut som börjar med data-. De nås via egenskapen dataset: <li data-id="42"> ger item.dataset.id === '42'.

Det bästa sättet att ändra hur ett element ser ut är oftast att byta CSS-klass och låta reglerna ligga i CSS-filen. Egenskapen classList har metoderna du behöver:

Metod Gör
add('a', 'b') lägger till en eller flera klasser
remove('a') tar bort en klass
toggle('a') lägger till om den saknas, tar bort om den finns
toggle('a', villkor) lägger till om villkoret är sant, annars tar bort
contains('a') true om klassen finns
replace('a', 'b') byter en klass mot en annan

Undvik att sätta className eller setAttribute('class', ...). Båda ersätter hela klasslistan:

// <div class="important">
box.className = 'danger' // <div class="danger">, important is gone
box.classList.add('danger') // <div class="important danger">

Du vet inte alltid vilka andra klasser ett element har, eller vem som har satt dem. classList låter dem vara.

Varje element har egenskapen style, som motsvarar style-attributet i HTML. CSS-egenskaper med bindestreck skrivs i camelCase, eftersom bindestreck inte är tillåtet i en identifierare:

element.style.color = '#aa5698'
element.style.fontSize = '1.25rem' // font-size
element.style.marginLeft = '2em' // margin-left
element.style.setProperty('--progress', '40%') // custom property

CSS-egenskapen float heter cssFloat i JavaScript, eftersom float var ett reserverat ord i äldre versioner av språket. Med setProperty() skriver du egenskapsnamnet precis som i CSS.

Sätt stil direkt bara när värdet verkligen räknas ut i koden, till exempel en position eller en procentsats. Allt annat hör hemma i CSS, och du byter klass. Det håller isär presentation och beteende, och du hittar alla stilregler på ett ställe. För att dölja och visa ett element finns dessutom egenskapen hidden: element.hidden = true. Tänk på att en CSS-regel med display på elementet tar över, så att elementet syns ändå.

  • Skapa med createElement, bygg klart och sätt in sist.
  • append, prepend, before, after, remove och replaceChildren täcker det mesta. Att sätta in en nod som redan finns flyttar den.
  • cloneNode(true) kopierar hela trädet under noden, cloneNode(false) bara noden.
  • Använd textContent för text. innerHTML bara med strängar du själv har skrivit.
  • Egenskaper som href och value ger tolkade och aktuella värden, attributen ger det som står i HTML-koden.
  • Byt klass med classList i stället för att sätta stil direkt.
CCBY