Webbteknik 2

Laboration 4A

DOM-objekt

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.

  1. Skapa själv en fil index.html med grundplåten för ett html-dokument

    • Tips: Använd förkortningen ! i VSCode för att snabbt skapa en grundstruktur.
  2. länka in script.js i head med <script type="module" src="script.js"></script> och skapa en tom script.js.

  3. länka in style.css i head med <link rel="stylesheet" href="style.css">

  4. Lägg in följande element i body:

    • En h1 rubrik med texten “Vinterkläder”
    • Ett section-element med id="clothes"
  5. Ö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.

  1. Skapa nio figure-element inuti section i html-dokumentet.

  2. I script.js, använd document.querySelector med selektorn "#clothes figure", och spara resultetet i en variabel element. Logga elementet i konsolen.

    Förslag på lösning
    const element = document.querySelector("#clothes figure");
    console.log(element);
    
  3. Som du ser i konsolen, så returnerar querySelector ett element, det första elementet som matchar selektorn. (Om ingenting loggas, kontrollera att du har skapat figure-elementen inuti section-elementet i html-filen.)

  4. Vi kan enkelt påverkar elementet genom att använda det returnerade objektet. Testa att ändra textContent på elementet till “Första figuren”:

    Förslag på lösning
    element.textContent = "Första figuren";
    
  5. Prova nu att istället använda document.querySelectorAll för att hämta alla element som matchar selektorn "#clothes figure". Spara resultatet i en variabel elements och logga den i konsolen.

    Förslag på lösning
    const elements = document.querySelectorAll("#clothes figure");
    console.log(elements);
    
  6. Som du ser i konsolen, så returnerar querySelectorAll en lista med alla element som matchar selektorn. Listan är av typen NodeList, 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 av elements[0] till exempel, eller skapa en loop som körs för varje element i listan. Vi kan loopa igenom listan med en for-loop eller med en for...of-loop.

  7. Använd en for...of-loop för att loopa igenom alla element i listan elements, och sätt textContent på 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 variabeln elem fö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.

  1. 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.css efter andra klasser som är påverkar för figure-elementen.

  • Testa att byta ut klassen i anropen till classList.toggle någon annan.
  • Testa att använda classList.add istället för toggle.

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.

  1. Börja med att kommentera bort all kod du skrivit hittills i script.js.
  2. 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>
    
  3. Kopiera och klistra in samma innehåll i de andra figure-element.
  4. Ändra src, alt och data-english attributen i img-taggarna samt texten i figcaption så att du har ett figure-element för varje bild i clothes-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
    
  5. 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.
  6. 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”.

  1. 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.getElementById för att hämta DOM-objekten för span-elementen med id current, prev och next. 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");
    
  2. Sedan behöver vi lägga till klick-hanterare på alla img-element.

    1. Använd document.querySelectorAll för att hämta alla img-element inuti #clothes. Spara listan med element i en variabel images.
    2. Loopa igenom images med en for...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ända classList.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");
        });
    }
    

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 parentElementfigure för att backa längre upp i trädstrukturen och komma åt section-elementet (figures förälder).
  • Lägg själv till en klass i style.css för section-elementet, och använd classList.add för att lägga till den klassen på section nä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.

  1. Använd figure.querySelector("figcaption").textContent i slutet av klick-hanteraren för att hämta texten i den figcaption som hör till den klickade bilden. Spara resultatet i en variabel name.
  2. Uppdatera textContent på objektet current, sätt det till värdet som finns i name variabeln. Så att namnet som hör till den klickade bilden visas i informationsrutan.
  3. 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.

  1. Inuti klick-hanteraren, efter att vi uppdaterat current.textContent, lägg till kod för att hämta det föregående syskonet till figure med previousElementSibling. Spara det i en variabel prevFigure.
  2. Kolla om prevFigure inte är null med hjälp av en if-sats.
  3. Om det inte är null, hämta texten i figcaption inuti prevFigure och uppdatera textContentprev-elementet med det värdet. Alltså i princip samma sak som vi gjorde för current ovan.

    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 problemOm vi klickat på ett plagg som inte har något föregående syskon (dvs det första), så kommer texten i prev-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 = "";
}
  1. Använd nextElementSibling för att implementera motsvarande funktionalitet för namnet i syskonet näst efter figure:

    1. Hämta det nästkommande syskonet med nextElementSibling och spara i en variabel nextFigure.
    2. Kolla om nextFigure inte är null.
    3. Om det inte är null, hämta texten från figcaption inuti nextFigure och uppdatera texten i next-elementet.
    4. Annars, sätt next.textContent till 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 = "";
        }
    

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.

  1. Inuti klick-hanteraren, innan vi uppdaterat current.textContent, lägg till kod för att hämta det engelska namnet från data-english attributet på den klickade bilden. Använd imgElement.dataset.english för att hämta värdet, och spara det i en variabel englishName.

  2. Uppdatera current.textContent så att det visar både det svenska och engelska namnet, t.ex. ‘Sko (engelska: “Boot”)’. Använd variablerna name och englishName fö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
  1. Använd document.getElementById för att hämta knappen med id clear.
  2. Använd addEventListener för att lägga till en klick-hanterare på knappen.
  3. I klick-hanteraren, använd document.querySelectorAll för att hämta alla figure-element.
  4. Loopa igenom listan med figure-element och använd classList.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
  1. Använd document.getElementById för att hämta knappen med id show-selected.
  2. Använd addEventListener för att lägga till en klick-hanterare på knappen.
  3. I klick-hanteraren, använd document.querySelectorAll("#clothes figure.selected") för att hämta alla element av typen figure som har klassen selected, spara listan med element i en variabel.
  4. Skapa en tom strängsvariabel names som vi kan bygga upp med namn.
  5. Loopa igenom listan och gör följande:
    1. Hämta texten i figcaption inuti det aktuella elementet.
    2. Lägg till namnet i names-variabeln, följt av ett mellanslag.
  6. Efter loopen, använd document.getElementById för att hämta selected-names-elementet och sätt dess textContent till värdet i names-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?