DOM ja tapahtumat - 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:

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:

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>
bubbling
bubbling

Esim. jos hiirellä klikataan dokumentin submit-painiketta etenee click-tapahtuma seuraavassa järjestyksessä eri elementtien kautta:

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!

Bubbling and capturing

How JavaScript Event Delegation Works

Tärkeimmät tapahtumat

Event reference

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ä

Introduction to events

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

kapselointi, encapsulation.js

progagointi tapahtuman liikkuminen dom-puussa ylöspäin (bubbling)

Käyttäjien kommentit

Kommentoi Lisää kommentti
Informaatioteknologia - Jyväskylän yliopiston informaatioteknologian tiedekunta