Vi har använt DOM-element i många laborationer tidigare, främst genom att koppla händelser till knappar, men ocskå för att uppdatera deras innehåll med ny text eller html. I denna laborationen skall vi bekanta oss mer med de objekt som representerar elementen i vårt html-dokument. Ni kommer att själva få bygga upp samma typ av applikation som vi gjorde i föreläsningen.
I den här labben, försök först att lösa de olika stegen på egen hand. Om du kör fast, titta i förslagen som finns under varje steg.
0 - Skapa en arbetsmapp och förbered filerna
Innan du börjar, skapa en ny labb-mapp som du kan kalla lab-4a där du sparar filerna för just den här laborationen. Ladda även ner materialet till denna labb från länken till höger på sidan och packa upp det i din labb-mapp. Om du har gjort rätt skall du ha en style.css och en clothes-mapp med bilder i lab-4a.
-
Skapa själv en fil
index.htmlmed grundplåten för ett html-dokument- Tips: Använd förkortningen
!i VSCode för att snabbt skapa en grundstruktur.
- Tips: Använd förkortningen
-
länka in
script.jsi head med<script type="module" src="script.js"></script>och skapa en tomscript.js. -
länka in
style.cssi head med<link rel="stylesheet" href="style.css"> -
Lägg in följande element i body:
- En
h1rubrik med texten “Vinterkläder” - Ett
section-element med id="clothes"
- En
-
Öppna sedan html-sidan med
Live Server.
1 - Hämta en lista med element från dokumentet
document-objektet är ett inbyggt objekt i webbläsaren som representerar hela html-dokumentet. Med hjälp av document kan vi hämta ut element från dokumentet med olika metoder. De objekt som returneras kallas för DOM-objekt, och varje element i html-strukturen har alltså ett motsvarande DOM-objekt som vi kan inteagera med från javascript. Vi har tidigare sett hur vi kan hämta ett DOM-objekt med hjälp av elementets id: document.getElementById("..."), alternativt med en css-selektor: document.querySelector("..."). Gemensat för dessa metoder är att de bara hämtar ett enda element. Ibland vill vi dock hämta flera element. För detta finns metoden: document.querySelectorAll("..."), som returnerar en lista med alla element som matchar selektorn.
Nu skall vi testa querySelectorAll och jämföra med querySelector. Men vi måste först lägga till lite innehåll i vår section att experimentera med.
-
Skapa nio
figure-element inutisectioni html-dokumentet. -
I
script.js, använddocument.querySelectormed selektorn"#clothes figure", och spara resultetet i en variabelelement. Logga elementet i konsolen.Förslag på lösning
const element = document.querySelector("#clothes figure"); console.log(element); -
Som du ser i konsolen, så returnerar
querySelectorett element, det första elementet som matchar selektorn. (Om ingenting loggas, kontrollera att du har skapatfigure-elementen inutisection-elementet i html-filen.) -
Vi kan enkelt påverkar elementet genom att använda det returnerade objektet. Testa att ändra
textContentpå elementet till “Första figuren”:Förslag på lösning
element.textContent = "Första figuren"; -
Prova nu att istället använda
document.querySelectorAllför att hämta alla element som matchar selektorn"#clothes figure". Spara resultatet i en variabelelementsoch logga den i konsolen.Förslag på lösning
const elements = document.querySelectorAll("#clothes figure"); console.log(elements); -
Som du ser i konsolen, så returnerar
querySelectorAllen lista med alla element som matchar selektorn. Listan är av typenNodeList, vilket är en speciell typ av lista som liknar en array. Ni kan använda den precis som om den vore en array, men det har inte alla metoder som en vanlig array har. För att komma åt individuella objekt behöver vi nu antingen indexera in i listan med hjälp avelements[0]till exempel, eller skapa en loop som körs för varje element i listan. Vi kan loopa igenom listan med enfor-loop eller med enfor...of-loop. -
Använd en
for...of-loop för att loopa igenom alla element i listanelements, och sätttextContentpå varje element till “En figur”.Förslag på lösning
for (const elem of elements) { elem.textContent = "En figur"; }Koden i loopen kommer att köras en gång för varje element i listan
elements. Och inuti loopen använder du variabelnelemför att referera till det aktuella elementet.
Med hjälp av querySelectorAll kan vi alltså snabbt och smidigt hämta ut flera element från dokumentet och manipulera dem på olika sätt.
2. Lägga till och ta bort css-klasser med classList-egenskapen
classList-egenskapen provade vi på redan i första kursveckan. Det är en väldigt användbar egenskap som alla html-element har. Den exponerar metoder för att lägga till, ta bort eller växla (dvs ta bort om den finns, eller lägga till om den inte finns) CSS-klasser på elementet.
Vi kan först testa att lägga till en klass på ett element. Vi har fortfarande en variabel element som refererar till det första figure-elementet i vår lista. Använd classList.toggle(...) för att lägga till klassen "selected" på elementet:
element.classList.toggle("selected");
Det första elementet borde nu ha fått en annan bakgrundsfärg tack vare att klassen "selected" har lagts till.
Nu skall vi lägga till kod för varje figure-element som växlar en CSS-klass när vi klickar på dem. Tack vare att vi med loopen kan köra samma kod för varje element, kan vi enkelt lägga klick-hanterare på alla elementen utan att behöva upprepa oss nio gånger.
- I loopen som vi skapade ovan, lägg till en klick-händelsehanterare på det aktuella elementet som växlar klassen
"selected"på elementet när det klickas.Förslag på lösning
Efter uppdateringen kan loopen se ut så här:for (const elem of elements) { elem.textContent = "En figur"; elem.addEventListener("click", function () { elem.classList.toggle("selected"); }); }
Testa själv Kolla i
style.cssefter andra klasser som är påverkar förfigure-elementen.
- Testa att byta ut klassen i anropen till
classList.togglenågon annan.- Testa att använda
classList.addistället förtoggle.
2. Navigera i DOM-trädet
Som du vet sedan Webbteknik 1 så består ett html-dokument av en trädstruktur av element, där element kan innehålla andra element som barn. Ibland vill vi kunna navigera i detta träd för att hitta relaterade element. Vi kan till exempel vilja hitta ett elements förälder, eller dess syskon, eller alla dess barn. På alla DOM-objekt finns egenskaper som låter oss göra precis detta.
Innan vi börjar experimentera med detta skall vi återställa koden och lägga till lite mer innehåll i våra figure-element.
- Börja med att kommentera bort all kod du skrivit hittills i
script.js. - I det första
figure-elementet i html-filen lägger du in följande innehåll:<img src="clothes/boot.png" alt="Sko" data-english="Boot"> <figcaption>Sko</figcaption> - Kopiera och klistra in samma innehåll i de andra
figure-element. - Ändra
src,altochdata-englishattributen iimg-taggarna samt texten ifigcaptionså att du har ett figure-element för varje bild iclothes-mappen. Följande är rimliga svenska översättningar av de olika plaggen:boot -> Sko coat -> Rock earmuffs -> Öronmuffar knit cap -> Mössa jacket -> Jacka jumper -> Tjocktröja mittens -> Vantar sweater -> Tröja trousers -> Byxor - Spara filen och ladda om sidan i webbläsaren för att se att allt ser ok ut. Du skall ha bild och bildtext i alla nio figure-element.
- Lägg också in en till sektion i slutet av
body, där vi kan skriva ut information om det klickade plagget senare:<section id="info"> <p>Klickat plagg: <span id="current"></span></p> <p>Plagg före: <span id="prev"></span></p> <p>Plagg efter: <span id="next"></span></p> </section>
Nu är det dagas att lägga till ny kod i script.js. Det funktionalitet vi skall implementera är att när användaren klickar på en bild så skall vi markera den figuren (som ovan), och skriva ut lite information om figuren i sig, men även om de närmsta “syskonen”.
-
Det första vi skall göra är att hämta DOM-objekten för de element vi skall skriva ut information i. Använd
document.getElementByIdför att hämta DOM-objekten förspan-elementen med idcurrent,prevochnext. Spara dem i variabler med samma namn.Förslag på lösning
const current = document.getElementById("current"); const prev = document.getElementById("prev"); const next = document.getElementById("next"); -
Sedan behöver vi lägga till klick-hanterare på alla
img-element.- Använd
document.querySelectorAllför att hämta allaimg-element inuti#clothes. Spara listan med element i en variabelimages. - Loopa igenom
imagesmed enfor...of-loop. Inuti loopen, lägg till en klick-händelsehanterare på varje element som loggar"Bild klickad!". Dvs precis samma sak som vi gjorde ovan med figure-elementen, fast nu loggar vi bara en text istället för att användaclassList.toggle.
Förslag på lösning
const images = document.querySelectorAll("#clothes img"); for (const imgElement of images) { imgElement.addEventListener("click", function () { console.log("Bild klickad"); }); } - Använd
Komma åt överliggande element med egenskapen parentElement
Selektorn ovan ger oss alla img-element. Men när vi klickar på en bild vill vi kunna komma åt det figure-element som bilden ligger i, så att vi kan sätta css-klassen selected som vi gjort i förra övningen. Vi kan komma åt ett elements förälder med egenskapen parentElement på DOM-objektet.
Använd imgElement.parentElement för att komma åt det överliggande figure-elementet inuti klick-hanteraren och spara i en variabel figure. Växla klassen "selected" på det figure när bilden klickas, som vi gjorde ovan.
Förslag på lösning
Efter uppdateringen kan klick-hanteraren se ut så här:
imgElement.addEventListener("click", function () {
console.log("Bild klickad");
const figure = imgElement.parentElement;
figure.classList.toggle("selected");
});
Testa själv: Backa längre upp i trädstrukturen.
- Använd
parentElementpåfigureför att backa längre upp i trädstrukturen och komma åtsection-elementet (figures förälder).- Lägg själv till en klass i
style.cssförsection-elementet, och användclassList.addför att lägga till den klassen påsectionnär en bild klickas.
Använd querySelector på ett element för att hitta barn
Hittills har vi bara använt document.querySelector och document.querySelectorAll för att hämta element från hela dokumentet. Men vi kan även använda dessa metoder på vilket DOM-objekt som helst för att hämta barn-element som matchar den angivna selektorn.
Nu skall vi läsa ut texten i figcaption-elementet som hör till den klickade bilden.
- Använd
figure.querySelector("figcaption").textContenti slutet av klick-hanteraren för att hämta texten i denfigcaptionsom hör till den klickade bilden. Spara resultatet i en variabelname. - Uppdatera
textContentpå objektetcurrent, sätt det till värdet som finns inamevariabeln. Så att namnet som hör till den klickade bilden visas i informationsrutan. - Ta bort den tidigare loggningen av
"Bild klickad".Förslag på lösning
Efter uppdateringen kan klick-hanteraren se ut så här:imgElement.addEventListener("click", function () { const figure = imgElement.parentElement; figure.classList.toggle("selected"); const name = figure.querySelector("figcaption").textContent; current.textContent = name; });
Komma åt syskon med egenskaperna nextElementSibling och previousElementSibling
Det finns som sagt också egenskaper på DOM-objekt som låter oss komma åt ett elements närmsta syskon i DOM-trädet. Dessa egenskaper heter nextElementSibling och previousElementSibling. Med dessa behöver vi vara lite försiktiga, eftersom alla element kanske inte har något föregående eller nästkommande syskon. I sådana fall kommer egenskapen att vara null. Så innan vi försöker göra något åt ett syskon-element, bör vi alltid kolla om det faktiskt finns.
Nu skall vi använda dessa egenskaper för att visa namnen på de närmsta syskonen till det klickade elementet i informationsrutan.
- Inuti klick-hanteraren, efter att vi uppdaterat
current.textContent, lägg till kod för att hämta det föregående syskonet tillfiguremedpreviousElementSibling. Spara det i en variabelprevFigure. - Kolla om
prevFigureinte ärnullmed hjälp av enif-sats. - Om det inte är
null, hämta texten ifigcaptioninutiprevFigureoch uppdateratextContentpåprev-elementet med det värdet. Alltså i princip samma sak som vi gjorde förcurrentovan.Förslag på lösning
Efter uppdateringen kan klick-hanteraren se ut så här:imgElement.addEventListener("click", function () { const figure = imgElement.parentElement; figure.classList.toggle("selected"); const name = figure.querySelector("figcaption").textContent; current.textContent = name; const prevFigure = figure.previousElementSibling; if (prevFigure !== null) { const prevName = prevFigure.querySelector("figcaption").textContent; prev.textContent = prevName; } });
När du klickar på olika bilder nu så skall namnet på det klickade plagget visas i rutan, samt namnet på det föregående plagget om det finns något.
Reflektera: Finns det något problem men denna implementation? Klicka runt på olika plagg och se vad som händer i informationsrutan.
Visa problem
Om vi klickat på ett plagg som inte har något föregående syskon (dvs det första), så kommer texten iprev-elementet att vara det som sattes vid förra klicket. Dvs den texten rensas inte bort. Fundera på hur du skulle kunna lösa detta.Förslag på lösning
Lägg till en else-sats som sätter prev.textContent till en tom sträng om prevFigure är null.
if (prevFigure !== null) {
const prevName = prevFigure.querySelector("figcaption").textContent;
prev.textContent = prevName;
}
else {
prev.textContent = "";
}
-
Använd
nextElementSiblingför att implementera motsvarande funktionalitet för namnet i syskonet näst efterfigure:- Hämta det nästkommande syskonet med
nextElementSiblingoch spara i en variabelnextFigure. - Kolla om
nextFigureinte ärnull. - Om det inte är
null, hämta texten från figcaption inutinextFigureoch uppdatera texten inext-elementet. - Annars, sätt
next.textContenttill en tom sträng.
Förslag på lösning
Efter uppdateringen skall det ligga ny kod sist i klick-hanteraren:const nextFigure = figure.nextElementSibling; if (nextFigure !== null) { const nextName = nextFigure.querySelector("figcaption").textContent; next.textContent = nextName; } else { next.textContent = ""; } - Hämta det nästkommande syskonet med
3. Egen data kopplad till html-element
Som du kanske har märkt i html-koden för img-elementen, så finns det ett attribut som heter data-english. Detta är ett så kallat “data-attribut”, vilket är ett sätt att koppla egen data till html-element. Data-attribut börjar alltid med data-, och kan sedan ha valfritt namn efter det. I detta fall har vi alltså kopplat engelska namnet på plagget till varje bild genom att använda data-english attributet.
Vi kan komma åt data-attributen från javascript genom att använda egenskapen dataset på DOM-objektet. Egenskapen dataset är ett objekt som innehåller alla data-attribut som finns på elementet. Varje attribut blir en egenskap på dataset-objektet, dvs data-english blir till dataset.english.
Nu skall vi använda detta för att visa det engelska namnet på det klickade plagget i informationsrutan.
-
Inuti klick-hanteraren, innan vi uppdaterat
current.textContent, lägg till kod för att hämta det engelska namnet fråndata-englishattributet på den klickade bilden. AnvändimgElement.dataset.englishför att hämta värdet, och spara det i en variabelenglishName. -
Uppdatera
current.textContentså att det visar både det svenska och engelska namnet, t.ex. ‘Sko (engelska: “Boot”)’. Använd variablernanameochenglishNameför att sätta texten.Förslag på lösning
Efter uppdateringen kan början på klick-hanteraren se ut så här:const figure = imgElement.parentElement; figure.classList.toggle("selected"); const name = figure.querySelector("figcaption").textContent; const english = imgElement.dataset.english; current.textContent = `${name} (engelska: "${english}")`;
4. Utmaningar
Slutligen kommer här några utmaningar som du kan prova när du känner dig bekväm med det vi gått igenom hittills.
Till att börja med skall vi lägga till en ny sektion i slutet av body i html-filen, där vi kan lägga till knappar som vi kan koppla kod till i utmaningarna.
<section id="challenges">
</section>
Utmaning 1 - Rensa alla markeringar
Skapa en knapp som rensar alla markeringar (dvs tar bort klassen selected från alla figure-element).
Lägg in följande knapp i challenges-sektionen i html-filen:
<button id="clear">Rensa alla markeringar</button>
Försök sedan att i script.js implementera en klick-hanterare för knappen ovan som tar bort klassen selected från alla figure-element när knappen klickas (använd classList.remove för att ta bort en klass från ett element).
Tips
- Använd
document.getElementByIdför att hämta knappen med idclear. - Använd
addEventListenerför att lägga till en klick-hanterare på knappen. - I klick-hanteraren, använd
document.querySelectorAllför att hämta allafigure-element. - Loopa igenom listan med
figure-element och användclassList.remove("selected")för att ta bort klassen från varje element.
Förslag på lösning
const btnClear = document.getElementById("clear");
btnClear.addEventListener("click", function () {
const figures = document.querySelectorAll("#clothes figure");
for (const fig of figures) {
fig.classList.remove("selected");
}
});
Utmaning 2 - Hitta alla valda plagg och visa deras namn
Skapa en knapp som, när den klickas, hittar alla markerade plagg och visar deras namn info-sektionen.
Börja med att lägga in följande knapp i challenges-sektionen i html-filen:
<button id="show-selected">Visa valda plagg</button>
…och följande paragraf i info-sektionen:
<p>Valda plagg: <span id="selected-names"></span></p>
Försök sedan att i script.js implementera klick-hanteraren.
Översiktligt tips
Klickhanteraren behöver hämta alla elementen som är av typen figure och har klassen selected. Därefter behöver den hämta namnet på plagget i varje figure och bygga upp en sträng med alla namn. Slutligen behöver den uppdatera textContent på det nya selected-names-elementet med den strängen.
Steg för steg tips
- Använd
document.getElementByIdför att hämta knappen med idshow-selected. - Använd
addEventListenerför att lägga till en klick-hanterare på knappen. - I klick-hanteraren, använd
document.querySelectorAll("#clothes figure.selected")för att hämta alla element av typenfiguresom har klassenselected, spara listan med element i en variabel. - Skapa en tom strängsvariabel
namessom vi kan bygga upp med namn. - Loopa igenom listan och gör följande:
- Hämta texten i
figcaptioninuti det aktuella elementet. - Lägg till namnet i
names-variabeln, följt av ett mellanslag.
- Hämta texten i
- Efter loopen, använd
document.getElementByIdför att hämtaselected-names-elementet och sätt desstextContenttill värdet inames-variabeln.
Förslag på lösning
const btnShowSelected = document.getElementById("show-selected");
function showSelected() {
const selectedFigures = document.querySelectorAll("#clothes figure.selected");
let names = "";
for (const fig of selectedFigures) {
const name = fig.querySelector("figcaption").textContent;
names += name + " ";
}
document.getElementById("selected-names").textContent = names;
}
btnShowSelected.addEventListener("click", showSelected);
I den här lösningen har jag valt att skapa en namngiven funktion, showSelected, för klick-hanteraren.
Reflektera: Varför kan det vara en bra idé att skapa en namngiven funktion istället för att använda en anonym funktion direkt i addEventListener?