Webbteknik 2

Laboration 4B

Händelser och Bubbling

I denna laboration ska vi titta närmare på hur händelser (events) fungerar i webbläsaren. Vi kommer att utgå från ett enkelt “måltavla”-spel där vi ska klicka på olika färgade fält för att samla poäng.

0 - Förberedelser

Skapa en ny mapp lab-4b och ladda ner startmaterialet (lab4b.zip). Packa upp filerna i mappen. Du ska ha index.html och style.css. Skapa även en tom fil script.js och länka in den i index.html (glöm inte type="module").

1 - Event-objektet

När en händelse inträffar (t.ex. ett klick) skapar webbläsaren ett event-objekt som innehåller information om händelsen. Detta objekt skickas med som argument till din händelsehanterare (callback-funktionen).

  1. Hämta elementet med id board och spara i en variabel.
  2. Lägg till en klick-lyssnare på board.
  3. I lyssnarfunktionen, ta emot event-objektet (ofta kallat e eller event) och logga det till konsolen.
Förslag på lösning
const board = document.querySelector("#board");

board.addEventListener("click", function(event) {
  console.log(event);
});
  1. Klicka på måltavlan och inspektera objektet i konsolen. Titta särskilt på egenskaperna target och currentTarget.
    • target: Elementet som faktiskt klickades på (t.ex. den gula cirkeln).
    • currentTarget: Elementet som lyssnaren sitter på (i detta fall #board). Observera att om du loggar hela event-objektet och sedan inspekterar det i konsolen, kan currentTarget ibland visa null eftersom objektet loggas som en referens och dess egenskaper kan ändras över tid. För att se det korrekta värdet, logga event.currentTarget direkt.

Testa att logga event.target och event.currentTarget specifikt och klicka på olika färger.

2 - Bubbling (Händelsespridning)

Händelser i DOM:en “bubblar” uppåt. Det betyder att om du klickar på den innersta cirkeln (svart), så triggas först klick-händelsen på den svarta cirkeln, sedan på den gula, sedan röda, blåa, och till sist på #board och body osv.

Vi ska testa detta genom att lägga lyssnare på alla cirklar.

  1. Börja med att kommentera bort koden du skrev ovan.
  2. Hämta alla element med klassen target (cirklarna har både klassen target och en färg-klass).
  3. Loopa igenom dem och lägg till en klick-lyssnare på varje.
  4. I lyssnaren, logga exempelvis event.currentTarget.className så att du ser vilken färg cirkeln har.
Förslag på lösning
const targets = document.querySelectorAll(".target");

for (const circle of targets) {
  circle.addEventListener("click", function(event) {
    console.log("Klickade på en cirkel:", event.currentTarget.className);
  });
}
  1. Klicka nu på den svarta cirkeln (mitten). Hur många loggar får du? Varför? Du borde se loggar för alla cirklar som ligger “under” muspekaren, från den innersta och utåt. Detta är bubbling.

3 - Räkna poäng

Om vi nu skulle vilja ge poäng baserat på vilken färg man träffar så får vi vara lite försiktiga. Säg att vi vill ge följande poäng:

  • Svart: 100 poäng
  • Gul: 50 poäng
  • Röd: 20 poäng
  • Blå: 10 poäng

Börja med att på elementen i html-dokumentet lägga in data-score-attribut med rätt poäng på varje cirkel.

Som vi sett ovan så kommer ett klick på en cirkel att triggera händelsen på alla färger som ligger “under” muspekaren, från den innersta och utåt. Detta kommer nog innebära problem för vår poängräkning. Men vi provar och ser vad som händer.

I script.js, gör följande:

  1. Högst upp, skapa en global variabel score som börjar på 0.
  2. I klick-lyssnaren för cirklarna (som du skapade i steg 2):
    • läs ut data-score-attributet från den cirkel som klickades och konvertera det till ett nummer.
    • Logga poängen till konsolen.
    • Öka score med rätt antal poäng.
    • Uppdatera texten i elementet #score med den nya poängen.
Försök till lösning
let score = 0;
const scoreDisplay = document.querySelector("#score");
const targets = document.querySelectorAll(".target");

for (const circle of targets) {
  circle.addEventListener("click", function(event) {
    const points = Number(event.target.dataset.score); 
    console.log("Träff!", points);
    score += points;
    scoreDisplay.textContent = score;
  });
}

Oavsett om du valde event.target eller event.currentTarget att läsa ut poängen ifrån så kommer du att få en alldeles för hög summa. Detta beror på att händelsen bubblar uppåt och triggar alla lyssnare på vägen.

För att förhindra detta måste vi stoppa händelsen från att bubbla vidare.

  1. Uppdatera din klick-lyssnare genom att lägga till event.stopPropagation() som det första du gör i funktionen.
Förslag på lösning (med stoppad bubbling)
let score = 0;
const scoreDisplay = document.querySelector("#score");
const targets = document.querySelectorAll(".target");

for (const circle of targets) {
  circle.addEventListener("click", function(event) {
    event.stopPropagation();
    const points = Number(event.target.dataset.score); 
    console.log("Träff!", points);
    score += points;
    scoreDisplay.textContent = score;
  });
}

Testa spelet! Nu ska du bara få poäng för den specifika ring du klickar på.

3.5 - Alternativ lösning: “Event delegation”

Det finns ett annat, ofta bättre sätt att lösa detta på. Istället för att lägga lyssnare på varje enskilt element och stoppa bubblingen, kan vi utnyttja bubblingen till vår fördel. Vi kan lägga en enda lyssnare på behållaren (#board) och sedan kontrollera var klicket faktiskt skedde. Detta kallas för “Event delegation”.

  1. Kommentera bort din gamla kod i script.js och skriv en ny lösning som:
    • Bara har en enda addEventListener#board.
    • I lyssnaren, använd event.target för att se vad som klickades.
    • Kontrollera om det klickade elementet har ett data-score attribut (använd dataset.score).
    • Om det har det, öka poängen och uppdatera texten.
Förslag på lösning
let score = 0;
const scoreDisplay = document.querySelector("#score");
const board = document.querySelector("#board");

board.addEventListener("click", function (event) {
  const points = Number(event.target.dataset.score);

  if (points) {
    score += points;
    scoreDisplay.textContent = score;
    console.log("Träff!", points);
  }
});

Ser du hur mycket renare koden blev? Vi behöver inte loopa över element, och vi behöver inte bry oss om stopPropagation. Eftersom event.target alltid pekar på det innersta elementet som klickades, så får vi automatiskt rätt poäng.

4 - Utforskning

Nu är det dags att experimentera med andra typer av händelser.

Här är en lista på några händelser som kan vara intressanta att testa:

  • Mus-händelser: click, dblclick, mousedown, mouseup, mousemove
  • Hover-händelser: mouseenter och mouseleave
  • Tangentbords-händelser: keydown, keyup

Börja med att göra en funktion som du kan använda för att testa när de olika händelserna sker.

  1. I script.js, skapa en funktion logEvent som tar en händelse som argument och loggar händelsen typ till konsolen.
Förslag på lösning
function logEvent(event) {
  console.log(event.type);
}

Nu kan du lägga en lyssnare på #board för varje händelse och använd logEvent som callback. På så sätt kan du se i konsolen när varje händelse sker och försöka bekanta dig med de olika händelserna.

Fundera: Är det några events som du inte kan få att fungera? Varför tror du att det är så?

  • Prova att lägga keyup och keydown lyssnarna på document istället för #board. Blir det någon skillnad?
  1. När du känner bekantat dig med när de olika händelserna inträffar lägger du till en rad i logEventsom logga ut hela event-objektet.
    console.log(event);
    
  2. Experimentera runt lite till på sidan och titta på olika egenskaper i event-objektet för de olika händelserna.
    • Vilka egenskaper finns för mus-händelser jämfört med tangentbords-händelser?
    • Vilka egenskaper ändras nör du flyttar runt musen?
    • Vilka ändras när du trycker på olika tangenter?

Tangentbordsstyrning

Gör så att man kan återställa poängen till 0 genom att trycka på tangenten ‘R’.

Förslag: Börja med att utnyttja logEvent som du redan har kopplad för se vad som finns i event-objektet när du trycker på tangenten. Använd sedan den informationen i en ny lyssnare, specifik för keydown, för att kolla om det var ‘R’ som trycktes ned och nollställ poängen i så fall.

Tips
  1. Tangentbordshändelser hamnar oftast på document (eller specifika input-fält).
  2. Lyssna på keydowndocument.
  3. Kolla event.key för att se vilken tangent som trycktes ned.
Förslag på lösning
document.addEventListener("keydown", function(event) {
  if (event.key === "r" || event.key === "R") {
    score = 0;
    scoreDisplay.textContent = score;
    console.log("Poängen nollställdes!");
  }
});

Prova dig fram och se vad mer du kan göra!