Händelser i DOM:en
I förra artikeln såg du att en händelse blir en task i kön, som händelseloopen kör när det är dess tur. Den här artikeln handlar om din sida av saken: hur du talar om för webbläsaren vilken kod som ska köras när något händer, vad du får veta om händelsen och vägen den tar genom DOM-trädet.
Tidsåtgång: Ungefär 15 minuter.
Lyssna med addEventListener()
Section titled “Lyssna med addEventListener()”Alla noder i DOM:en ärver från EventTarget, och därmed har alla metoden addEventListener(). Den
tar namnet på händelsen och en funktion, en lyssnare (event listener), som webbläsaren anropar
varje gång händelsen inträffar:
const button = document.querySelector('button')
button.addEventListener('click', () => { console.log('The button was clicked.')})Händelserna kommer från olika håll. Användaren utlöser click, input och keydown. Webbläsaren
själv utlöser DOMContentLoaded när dokumentet är tolkat och load när en bild har laddats. Och din
egen kod kan utlösa egna händelser, vilket är ämnet för nästa artikel.
Händelsernas namn skrivs alltid med gemener.
Samma element kan ha hur många lyssnare som helst för samma händelse. De anropas i den ordning de lades till.
Inte i HTML-koden
Section titled “Inte i HTML-koden”I äldre kod ser du ibland lyssnaren skriven direkt i ett attribut, eller tilldelad till en egenskap:
<button onclick="save()">Spara</button>button.onclick = saveUndvik båda. Attributet blandar ihop struktur och beteende, kräver att save är en global funktion
och blockeras av många webbplatsers säkerhetsinställningar (Content Security Policy). Egenskapen
rymmer bara en funktion, så nästa tilldelning skriver över den förra. addEventListener() har ingen
av de nackdelarna.
Fråga: varför händer inget vid klick?
Section titled “Fråga: varför händer inget vid klick?”function greet() { console.log('Hej!')}
button.addEventListener('click', greet())Hej! skrivs ut en gång när sidan laddas, men aldrig när knappen klickas. Varför?
Parenteserna efter greet anropar funktionen direkt, när raden körs. Det som skickas till
addEventListener() är funktionens returvärde, undefined, och inte funktionen. Webbläsaren har
alltså ingen lyssnare att anropa vid klick, och den säger inte heller ifrån.
Skicka en referens till funktionen, addEventListener('click', greet), eller en pilfunktion som
anropar den, addEventListener('click', () => greet()).
Händelseobjektet
Section titled “Händelseobjektet”Lyssnaren får ett argument: ett händelseobjekt med information om vad som hänt. Vilka egenskaper
det har beror på händelsen. Ett MouseEvent har till exempel muspekarens position, och ett
KeyboardEvent vilken tangent som trycktes ned. Några egenskaper har alla händelser:
| Egenskap | Innehåll |
|---|---|
type |
Händelsens namn, till exempel 'click'. |
target |
Elementet där händelsen inträffade, till exempel det som klickades på. |
currentTarget |
Elementet som lyssnaren sitter på. |
target och currentTarget är samma element när du lyssnar direkt på det som klickas. Skillnaden
blir viktig när händelsen har färdats genom trädet, som du ser längre ned.
this i en lyssnare
Section titled “this i en lyssnare”I en lyssnare skriven som en vanlig funktion är this elementet som lyssnaren sitter på, samma som
event.currentTarget. En pilfunktion har inget eget this, utan använder this från koden runt
omkring. I en klass, där this ska vara objektet och inte elementet, är det precis vad du vill:
class Counter { #count = 0
constructor(button) { button.addEventListener('click', () => this.increment()) }
increment() { this.#count++ }}Med en vanlig funktion hade this.increment letats upp på knappen och inte hittats. Äldre kod löser
det med const that = this eller .bind(this). Med pilfunktioner behövs ingetdera, och kursens kod
använder pilfunktioner för lyssnare i klasser. Vill du veta vilket element lyssnaren sitter på
använder du event.currentTarget.
Sluta lyssna
Section titled “Sluta lyssna”En lyssnare tas bort med removeEventListener(), som kräver exakt samma funktion som lades
till. En funktion som ser likadan ut räcker inte, eftersom två funktioner bara är lika om de är
samma objekt:
// Not removed – these are two different functions.button.addEventListener('click', () => save())button.removeEventListener('click', () => save())
// Removed – the same function.button.addEventListener('click', save)button.removeEventListener('click', save)En pilfunktion skriven direkt i anropet går alltså inte att ta bort med removeEventListener().
Alternativen once och signal
Section titled “Alternativen once och signal”Som tredje argument tar addEventListener() ett objekt med alternativ. Två av dem gör det enklare
att sluta lyssna:
button.addEventListener('click', () => save(), { once: true })Med once: true tas lyssnaren bort automatiskt efter första anropet.
const controller = new AbortController()const { signal } = controller
button.addEventListener('click', () => save(), { signal })form.addEventListener('submit', (event) => send(event), { signal })window.addEventListener('resize', () => resize(), { signal })
// Later: removes all three at once.controller.abort()Med signal kopplas lyssnaren till ett AbortController. När abort() anropas tas alla lyssnare
som fått samma signal bort, och du behöver inte spara en referens till varje funktion. Det är
mönstret du såg i kursvecka 10, och i
den sista artikeln ser du varför det passar så bra i en komponent.
Händelsens väg genom trädet
Section titled “Händelsens väg genom trädet”En händelse som inträffar på ett element stannar inte där. Klickar du på en knapp i en lista har du ju också klickat på listan, och på sidan. Händelsen färdas därför genom trädet i tre faser:
- Capture: från
windowned genom förfäderna till målet. - Målet: händelsen når elementet där den inträffade,
event.target. - Bubble: från målet upp genom förfäderna till
windowigen.
Lyssnare anropas som standard i bubble-fasen. Vill du fånga händelsen på vägen ned skickar du med
{ capture: true }. Det behövs sällan.
Inte alla händelser bubblar. focus, blur, mouseenter och mouseleave stannar vid målet. Vill
du lyssna på fokus längre upp i trädet finns focusin och focusout, som bubblar. MDN anger för
varje händelse om den bubblar.
Fråga: i vilken ordning?
Section titled “Fråga: i vilken ordning?”<ul> <li><button>Ta bort</button></li></ul>document.querySelector('ul').addEventListener('click', () => console.log('ul'))document.querySelector('li').addEventListener('click', () => console.log('li'))document.querySelector('button').addEventListener('click', () => console.log('button'))I vilken ordning skrivs namnen ut när knappen klickas? Och om lyssnaren på ul i stället läggs
till med { capture: true }?
Utan capture: button, li, ul. Händelsen når först målet och bubblar sedan uppåt.
Med capture på ul: ul, button, li. Lyssnaren på ul anropas på vägen ned, före målet. De
andra två anropas som förut.
Delegering
Section titled “Delegering”Bubblandet gör det möjligt att lyssna på många element med en enda lyssnare. I stället för att ge varje knapp i en lista en egen lyssnare lyssnar du på listan och tar reda på vilken knapp som klickades:
const list = document.querySelector('#todo-list')
list.addEventListener('click', (event) => { const button = event.target.closest('button') if (!button) { return }
button.closest('li').remove()})Det kallas delegering (event delegation) och har två fördelar. En lyssnare är billigare än hundra, och knappar som läggs till i listan senare fungerar direkt, utan att någon behöver komma ihåg att ge dem en lyssnare.
Här syns skillnaden mellan egenskaperna. event.currentTarget är listan, där lyssnaren sitter.
event.target är det innersta elementet som klickades på, och det är inte alltid knappen.
closest() från kursvecka 9 letar sig uppåt från det elementet
till närmaste knapp, eller ger null om klicket inte var på en knapp.
Fråga: varför försvinner inte raden?
Section titled “Fråga: varför försvinner inte raden?”Utan closest() ser lyssnaren ut så här:
list.addEventListener('click', (event) => { if (event.target.tagName === 'BUTTON') { event.target.parentElement.remove() }})Knapparna innehåller en ikon, <button><span class="icon">🗑</span> Ta bort</button>. Ibland
försvinner raden vid klick, ibland inte. Varför?
Klickar användaren på ikonen är event.target elementet <span>, inte <button>. Villkoret blir
falskt och ingenting händer. Klickar användaren på texten bredvid ikonen är målet knappen, och raden
försvinner.
event.target.closest('button') hittar knappen i båda fallen, eftersom closest() börjar med
elementet självt och sedan går uppåt.
Stoppa en händelse
Section titled “Stoppa en händelse”Två metoder på händelseobjektet låter som om de gör samma sak, men gör det inte.
preventDefault() stoppar webbläsarens förvalda beteende: att en länk navigerar, att en
kryssruta kryssas i eller att ett formulär skickas och sidan laddas om. Händelsen fortsätter att
färdas genom trädet som vanligt.
form.addEventListener('submit', (event) => { event.preventDefault() send(new FormData(form))})stopPropagation() stoppar händelsens väg genom trädet. Lyssnare längre bort på vägen anropas
inte. Det förvalda beteendet påverkas inte.
Behovet av preventDefault() är vanligt. Behovet av stopPropagation() är ovanligt, och det är
lätt att ställa till det med den. En delegerad lyssnare längre upp, en meny som stängs vid klick
utanför den eller ett analysverktyg som räknar klick slutar fungera utan att det syns varför.
Använd den inte utan ett tydligt skäl.
Fråga: vart tog utskriften vägen?
Section titled “Fråga: vart tog utskriften vägen?”form.addEventListener('submit', () => { console.log('Skickar...')})När formuläret skickas syns Skickar... i konsolen ett ögonblick och försvinner sedan. Vad hände?
Lyssnaren kördes, men sedan gjorde webbläsaren det förvalda: skickade formuläret och laddade om sidan. Konsolen töms när sidan laddas om, och alla variabler och all status från skriptet är borta.
event.preventDefault() först i lyssnaren stoppar omladdningen. Vill du behålla konsolens innehåll
mellan sidladdningar kan du slå på Preserve log i utvecklarverktygen.
Att ta med sig
Section titled “Att ta med sig”- Lyssna med
addEventListener()och skicka en funktion, inte resultatet av ett anrop. Undvikonclicki HTML-koden. event.targetär elementet där händelsen inträffade,event.currentTargetelementet där lyssnaren sitter. Använd pilfunktioner för lyssnare i klasser.removeEventListener()kräver samma funktion. Med{ signal }och ettAbortControllertar ett anrop tillabort()bort många lyssnare på en gång.- En händelse färdas ned till målet och bubblar sedan upp, om den bubblar. Det gör delegering
möjlig: en lyssnare på föräldern och
event.target.closest(). preventDefault()stoppar webbläsarens förvalda beteende.stopPropagation()stoppar händelsens väg och behövs sällan.
Vidare läsning
Section titled “Vidare läsning”- Läsanvisningarna för veckan, i synnerhet kapitel 15.2 i kursboken.
- Introduction to events och addEventListener() på MDN.
- Event reference på MDN, för att se om en händelse bubblar och vilket förvalt beteende den har.