DOM ja tapahtumat - esimerkkejä
- Skriptien lataaminen
- load-tapahtuma
- Tapahtuman eteneminen DOM-puussa
- Tärkeimmät tapahtumat
- Funktioiden ja lohkojen sisällä esitellyt funktiot
- click-tapahtuma
- Esimerkkejä
WWW-sivu ja selainympäristö ovat täynnä monenlaisia tapahtumia (events) joihin sivun ja sillä mahdollisesti toimivan javascript-sovelluksen täytyy reagoida. Joudumme opettelemaan tapahtumapohjaista ohjelmointia. Sovelluksen suorittaminen ei enää tapahdukkaan synkronisesti järjestyksessä vaan asynkronisesti.
Käy läpi Introduction to events-dokumentti.
Ensimmäinen tapahtuma, johon javascript-ohjelmoija törmää, on Window-objektin load-tapahtuma. WWW-sivuun liittyy tyypillisesti useita eri osia: varsinainen html-dokumentti, css-tiedostoja, kuvia, videoita, skriptejä jne. Javascript-ohjelman suorittaminen ei välttämättä ole mahdollista tai järkevää ennen kuin kaikki sivuun liittyvät osat ovat latautuneet. Selain lataa näitä osia yleensä asynkronisesti eli eri osasia voi latautua samaan aikaan. Poikkeuksena ovat skriptit, jotka oletuksena ladataan synkronisesti eli koko skripti ladataan ja suoritetaan ja vasta sitten siirrytään dokumentissa seuraavaan skriptiin tai muuhun osaan. Synkronisesti latautuva skripti blokkaa suorituksensa ajaksi kaiken muun.
Skriptien lataaminen
Skriptit voidaan ladata myös asynkronisesti. Käytössä on seuraavat vaihtoehdot:
- Ladataan ja suoritetaan synkronisesti. Blokataan html-dokumentin lataaminen
ja DOM-puun muodostaminen kunnes on suoritettu
Jos tämä skripti-elementti on heti dokumentin head-osassa, niin koodissa ei voi käsitellä mitään dokumentin body-osassa olevaa, koska body ei ole vielä latautunut eikä DOM-puu ole valmis.<script src="skripti.js"></script>
- Ladataan asynkronisesti eli samaan aikaan dokumentin muiden osien kera, mutta
suorittamisen ajaksi blokataan kuten edellisessä versiossa. Skriptien suoritusjärjestys
riippuu niiden latautumisesta eli sitä ei voi ennalta tietää.
<script async="" src="skripti.js"></script>
- Ladataan asynkronisesti, mutta suoritetaan vasta, kun DOM-puu on valmis
ja juuri ennen kuin DOMContentLoaded-tapahtuma tapahtuu.
Skriptit suoritetaan siinä järjestyksessä kuin ne esiintyvät HTML-koodissa.
<script defer="" src="skripti.js"></script>
- Toimitaan kuten edellä (defer)
<script async="" type="module" src="skripti.js"></script>
load-tapahtuma
Ensimmäinen javascript-ohjelmoijan käsittelemä tapahtuma on yleensä
load-tapahtuma,
jolla varmistetaan, että oma ohjelmakoodi suoritetaan vasta koko dokumentin
ja siihen liittyvien muiden objektien latauduttua ja DOM-puun valmistuttua. Tapahtumankäsittelijä asetetaan aina
addEventListener
-metodilla.
Muita tapoja ei pitäisi käyttää.
addEventListener
-metodi varmistaa, että mahdollisia muita
saman tapahtuman käsittelijöitä ei ylimääritellä. Samoin se mahdollistaa helpon
tavan poistaa tietty tapahtumankäsittelijä.
Parametreina annetaan tapahtumantyyppi merkkijonona ja osoitin tapahtumaa
käsittelevään funktioon.
// asetetaan load-tapahtumalle käsittelijäksi oma funktio anonyymifunktiona
window.addEventListener("load", function(e) {
// tänne oma koodi
console.log("ensimmäinen käsittelijä");
});
// sama kuin edellä, mutta käyttäen nimettyä funktiota
function omakoodi(e) {
// tänne oma koodi
console.log("toinen käsittelijä");
}
window.addEventListener("load", omakoodi);
Tapahtumakäsittelijä saa aina parametrinaan Event-tyyppisen objektin, jolta saa lisätietoja tapahtumasta. Tärkeimpiä ominaisuuksia ja metodeja ovat:
- currentTarget elementti, joka käsittelee tapahtumaa. Yleensä sama kuin tapahtumakäsittelijän this
- originalTarget
- target elementti, jolle tapahtuma tapahtui
- preventDefault() - peruu tapahtuman oletustoiminnan
- stopPropagation() - estää tapahtuman etenemisen DOM-puussa
Tarvittaessa tapahtumankäsittelijän voi poistaa removeEventListener-metodilla:
window.removeEventListener("load", omakoodi);
Kts. Matching event listeners for removal
Jos ei ole tarpeellista odottaa kuvien ja muiden lisäobjektien lataamista, voi käyttää DOMContentLoaded-tapahtumaa, joka tapahtuu heti kun DOM-puu on valmis.
Esimerkki load- ja DOMContentLoaded-tapahtumista
Tapahtuman oletustoiminnan estäminen (preventDefault()
)
Joskus tapahtuma halutaan estää. Tyypillisin tilanne on lomake, joka halutaan käsitellä javascript-ohjelmalla eikä lähettää lomakkeen tietoja palvelimelle, joka olisi oletustoimintatapa. Lomakkeen käsitteleminen tapahtuu submit-tapahtumassa, joka voidaan ottaa omaan hallintaan seuraavasti (esimerkki) kutsumalla preventDefault()-metodia tapahtuman käsittelijässä:
<script>
function omasubmit(e) {
// estetään seuraavalla lomakkeen lähettäminen palvelimelle
e.preventDefault();
console.log("tänne oma koodi käsittelemään lomaketta");
}
window.addEventListener("DOMContentLoaded", function() {
document.forms[0].addEventListener("submit", omasubmit);
});
</script>
<form method="post" action="https://foo.bar.example/">
<fieldset>
<label>Nimi <input type="text" value="" name="nimi" /></label>
<div><input type="submit" value="Lähetä" /></div>
</fieldset>
</form>
Tapahtuman eteneminen DOM-puussa
Oletuksena lähes kaikki tapahtumat toimivat bubbling-periaatteella eli ne siirtyvät DOM-puussa sisimmästä elementistä uloimpaan. Useimmilla tapahtumilla toimii myös tunneling-periaate, jossa siirrytään uloimmasta elementistä sisimpään, mutta sitä harvemmin käytetään. Näille periaatteille ei ole hyvää suomennosta.
Tapahtuman voi käsitellä missä vaiheessa tahansa bubblingia tai tunnelingia. Käytännössä tunneling-vaiheen voi jättää kokonaan huomiotta ja riittää, että keskittyy bubbling-vaiheeseen.
Seuraavanlaisessa HTML-dokumentissa voitaisiin click-tapahtuma käsitellä monessa eri vaiheessa.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fi" lang="fi">
<head>
<title>Bubbling ja tunneling</title>
</head>
<body>
<form method="post" action="https://foo.bar.example/">
<fieldset>
<label>Nimi <input type="text" value="" name="nimi" /></label>
<div><input type="submit" value="Lähetä" /></div>
</fieldset>
</form>
</body>
</html>
Esim. jos hiirellä klikataan dokumentin submit-painiketta etenee click-tapahtuma seuraavassa järjestyksessä eri elementtien kautta:
- Ensin tapahtuu tunneling ylhäältä alaspäin:
- html
- body
- form
- fieldset
- div
- input
- Seuraavaksi bubbling alhaalta ylöspäin
- input
- div
- fieldset
- form
- body
- html
Kokeile esimerkkiä ja katso esimerkin konsolin tulosteet.
<script>
"use strict";
let count = 1;
function omabubble(e) {
console.log("bubbling", count++, this);
}
function omatunnel(e) {
console.log("tunneling", count++, this);
}
window.addEventListener("DOMContentLoaded", function() {
let elems = document.querySelectorAll('*');
for(let elem of elems) {
elem.addEventListener("click", omabubble);
elem.addEventListener("click", omatunnel, true);
}
document.forms[0].addEventListener("submit", function(e) {e.preventDefault()});
});
</script>
<form method="post" action="https://foo.bar.example/">
<fieldset>
<label>Nimi <input type="text" value="" name="nimi" /></label>
<div><input type="submit" value="Lähetä" /></div>
</fieldset>
</form>
Mitä hyötyä bubblingista on? Voidaan käsitellä useiden elementtien tapahtumia ilman, että jokaiseen tarvitsee erikseen lisätä oma tapahtumakäsittelijä. Esim. seuraavassa esimerkissä käsitellään listan kaikkien li-elementtien click-tapahtumat ul-elementin kautta:
<script>
"use strict";
let count = 1;
function li_clicked(e) {
// e.target on elementti jota klikataan ja
// this tai e.currentTarget on elementti, joka käsittelee tapahtuman
console.log(e.target, this, e.currentTarget);
// tarkistetaan, että ei ole klikattu pelkkää ul-elementtiä
// jos klikattu jotain muuta niin käytetään elementtiä itseään laskurina
// monimutkaisemmassa rakenteessa pitäisi olla tarkempi
if ( e.target != e.currentTarget) {
e.target.textContent = parseInt(e.target.textContent) + 1;
}
}
// käsitellään kaikkien li-elementtien klikkailut ul-elementin kautta,
// koska click-tapahtuma bubbles ylöspäin DOM-puussa
window.addEventListener("DOMContentLoaded", function() {
let lista = document.getElementsByTagName("ul")[0];
lista.addEventListener("click", li_clicked);
});
</script>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
Joskus (erittäin harvoin) on tarpeellista estää bubbling. Tämä tapahtuu stopPropagation()-metodilla tai stopImmediatePropagation()-metodilla. Näiden käyttämiseen pitää olla hyvä syy!
How JavaScript Event Delegation Works
Tärkeimmät tapahtumat
- load
- DOMContentLoaded
- input
- change
- click
- submit
- mousedown
- mouseup
- dblclick
- animationstart
- animationend
- blur
- focus
- resize
- keydown
- keyup
- Drag and Drop -tapahtumat
Funktioiden ja lohkojen sisällä esitellyt funktiot
DOM-rajapinnan tapahtumakäsittelyssä joutuu esittelemään funktioita toisten funktioiden tai lohkojen sisällä. Tässä on mahdollisuus samankaltaisiin ongelmiin, kuin var-sanalla esiteltyjen muuttujien kanssa.
Tyypillinen tilanne olisi seuraava eli luodaan testifunc-funktio silmukan sisällä:
window.addEventListener("load", function() {
let elems = document.querySelectorAll("elem");
for(let elem of elems) {
// tämä on huono
function testifunc(e) {
console.log(elem, e);
};
elem.addEventListener("click", testifunc);
}
});
Tässä tapauksessa testifunc luodaan oikeasti jo paljon ennen silmukan suorittamista. Tässä voi olla eri ympäristöissä erilaisia toteutuksia, joka on pääsyy siihen, että edellä olevaa ei suositella. Tässä myös voi jäädä muistiin roikkumaan turhaan useita versioita samasta funktiosta.
Tämä kannattaakin muuttaa seuraavaan muotoon eli for-loopin sisällä oleva funktio esitelläänkin kuin muuttuja:
window.addEventListener("load", function() {
let elems = document.querySelectorAll("elem");
for(let elem of elems) {
// tämä on parempi
let testifunc = function(e) {
console.log(elem, e);
};
elem.addEventListener("click", testifunc);
}
});
Nyt funktio oikeasti luodaan vasta silmukan suoritusvaiheessa. Funktio on myös olemassa vain silmukan suorituksen ajan, kuten let-sanalla esitellyn muuttujan pitääkin.
click-tapahtuma
Lähes kaikilla html-elementeillä on click-tapahtuma.
<script>
function omaklik(e) {
console.log(e, e.target, e.originalTarget, e.currentTarget);
}
// koska käsiteltävä elementti on vasta koodin jälkeen, on muistettava
// käyttää load- tai DOMContentLoaded-tapahtumaa
window.addEventListener("DOMContentLoaded", function() {
let p = document.getElementById("click");
p.addEventListener("click", omaklik);
});
</script>
<p id="click">click this</p>
Esimerkkejä
Esimerkissä käytetään W3C:n DOM -spesifikaation metodeja ja attribuutteja, jotka toimivat moderneissa selaimissa.
Toinen esimerkki tietojen tallentamisesta objektiin
window.onload-tapahtuma sekä miten toimii nodelist-objekti, joka on live, nodelist.js
progagointi tapahtuman liikkuminen dom-puussa ylöspäin (bubbling)
- Elementtien käsitteleminen
- Dynaaminen lomakkeen luominen
- Listan numerointi
- Elementtien poistaminen
- Tapahtumankäsittelijät ( click, hover, blur, focus jne, katso: Events)
- Sivun ulkoasun muuttaminen ( avautuva / sulkeutuva lista, värin muutos )
Käyttäjien kommentit