Javascript ja DOM
Harjoitellaan Javascriptin ja DOM-rajapinnan perusteita.
Mallivastaus (mallivastaus.zip)
Alkuvalmistelut
- Käynnistä Firefox-selain. Seuraavat tehtävät toteutetaan Firefoxissa käyttäen W3C:n DOM-rajapintaa. Älä käytä valmiita Javascript-kirjastoja.
- Avaa avuksesi
- DOM
- DOM ja tapahtumankäsittely
- Lomakkeet
- DOM Reference
- Jos HTML:n ja CSS:n perusteet eivät ole muistissa niin vilkaise myös seuraavat:
Hello World
Tallenna käyttöösi valmis dokumenttipohja johonkin väliaikaishakemistoon.
- Valmiiseen pohjaan on linkitetty myös jshint, htmlinspector ja
W3C-validator. Saat automaattisesti konsoliin ilmoituksia mahdollisista virheistä.
Valmis pohja on XHTML5-yhteensopiva ns.
polyglot-dokumentti.
Xhtml-dokumenttia muokatessa teidän on muistettava noudattaa XML-kielioppia, joka on tiukempi kuin HTML5-kielen löperö standardi sallii. Esim. MDN:n sivuilla on joitakin esimerkkejä kirjoitettu XML-epäyhteensopivasti ja ne eivät suoraan kopioituna toimi xhtml-dokumentissa. Pyrimme koko kurssin ajan kirjoittamaan XHTML5-yhteensopivaa ns. polyglot-html-koodia, joka tarkoittaa, että sama dokumentti toimii text/html- ja application/xhtml+xml-mediatyypillä.
Mitä tarkoittaa mediatyyppi?
Mitä tarkoittaa mediatyyppi? Se tarkoittaa, että millaiseksi dokumentiksi www-palvelin väittää siltä lataamaamme tiedostoa, joka voi olla html-dokumentti, kuva, video, pdf-tiedosto jne. Windows-maailmassa on opittu, että tiedoston pääte määrittää tiedoston tyypin. Webbimaailmassa www-palvelin tai www-palvelimella suoritettava sovellus päättää viime kädessä mikä on käytettävä tyyppi. Voi olla, että mediatyyppi on päätelty suoraan tiedoston päätteestä tai sitten ei. Esim. onko kuva tarkoitettu esitettäväksi vaikkapa www-sivun osana (image/jpg) vai onko tarkoitus, että se vain tallennetaan, jolloin voidaan käyttää geneeristä tyyppiä (application/octet-stream). Palvelimen palauttaman mediatyypin perusteella selain päättää miten se toimii.
Tavallisen www-sivun (html-dokumentin) mediatyyppi on text/html. Selain parsii tämän tyypin sisällön löysästi eli ei herjaa mistään virheistä tai vioista vaan korjailee puutteet ja virheet parhaansa mukaan. Tämän takia suurin osa maailman www-sivuista on rakenteeltaan rikkinäisiä, koska virheistä ei kukaan koskaan huomauta mitään. Tämähän kuulostaa tavallaan hyvältä, mutta oikeasti tämä aiheuttaa ongelmia. Missä kohtaa sivu on rakenteeltaan niin pahasti rikki, että selaimet eivät enää osaa sitä korjata? Emme tiedä. Jokainen selain voi yrittää korjata rakennetta eri tavalla. Emme voi tietää mikä rikkoo minkäkin selaimen. Jos suomenkieltä kirjoittaa tai puhuu väärin, voi tulla silti aika hyvin oikein ymmärretyksi, mutta jossain vaiheessa virheiden raja tulee vastaan luonnollisessakin kielessä ja tuloksena on väärinymmärrys. Raja on jokaisella henkilökohtainen.
Rikkinäinen html-rakenne vaikuttaa olennaisesti myös CSS-tyylien toimintaan. Jos rakenne on rikki, ei voi tietää tarkkaan miten tyylit toimivat. Javascript-sovelluksen kanssa tilanne on sama: tarvitsemme DOM-rajapintaa, joka muodostuu HTML-dokumentin sisällön perusteella. Jos sisältö on rikki, voi olla, että myös DOM-rajapinta onkin jossain selaimessa erilainen kuin toisessa. On siis tärkeää pitää html-koodin rakenne ehjänä.
Mediatyyppi application/xhtml+xml aiheuttaa muutoksen. Tällä mediatyypillä jaetut dokumentit selain parsiikin xml-parserilla. xml-parseri ei yritä korjata yhtään virheitä vaan heittää heti pyyhkeen kehään, jos rakenne on rikkinäistä. Käyttäessämme .xhtml-päätteistä tiedostoa saamme users.jyu.fi-koneella toimivan apache-www-palvelimen jakamaan dokumenttimme application/xhtml+xml-mediatyypillä. Nyt selain ilmoittaa meille heti rakenteellisista virheistä ja ne tulee myös korjattua heti.
DOM-rajapinnan kautta ei pysty rikkomaan XML-dokumentin rakennetta, kunhan jättää käyttämättä metodit, joilla voi liittää suoraan kirjoitettua html-koodia DOM-puuhun. Esim. seuraava on tällä kurssilla ehdottomasti kiellettyä:
elementti.innerHTML = '<div class="foo">...<p>...</div>';
Edellinen aiheuttaisi xml-parserin kaatumisen. Tavallinen html-parseri sallisi edellisen vaikka rakenne onkin rikki (p-elementin lopetustagi puuttuu). Jonkun mielestä edellinen näyttää varmasti kätevältä ja aikaa säästävältä? Kyllä, tällä tavalla voisi jossain tilanteissa säästää koodirivejä. Tarkemmin ajateltuna tämä aiheuttaa kuitenkin lisää koodia: Miten hoidetaan erikoismerkit (<, > ja &)? Miten varmistetaan, että rakenne pysyy ehjänä? Normaali DOM-rajapinta pitää rakenteen automaattisesti ehjänä. Miten estetään, että käyttäjän syötteiden avulla ei dokumenttiin ujuteta vaarallista koodia? Joku syöttää lomakkeelle <script></script>-elementin ja kirjoittaa sen sisään vaarallista koodia. DOM-rajapinta huolehtii meillä kaikesta, jos käytämme sitä.
Selaimen inspector näyttää joitakin html-elementtejä eri muodossa, kuin kirjoitamme varsinaiseen xhtml-dokumenttiin. Tästä ei kannata välittää. Muistakaa kirjoittaa seuraavat elementit xhtml-dokumenttiin tässä annetussa muodossa:
<img src="..." /> <br /> <input type="..." /> <option value="..."></option>
Kaikilla elementeillä täytyy aina olla lopetustagi. Elementeillä, joilla ei voi olla sisältöä, lopetustagi sisällytetään suoraan aloitustagiin (img, br, input). HTML5-standardi sallii lopetustagien unohtelun, mutta XHTML5 ei salli.
Lisäksi muistakaa id-attribuuteista (esim.
<input id="foobar" .. />
) seuraavat asiat:- Jokainen id-attribuutti on oltava uniikki. Samassa dokumentissa ei voi olla kahta samaa id-attribuutin arvoa. getElementById-funktio hajoaa, jos näin ei ole
- id-attribuutin arvo ei saa alkaa numerolla. HTML5-standardissa tätä rajoitusta ei ole, mutta vanhemmissa standardeissa on. Kannattaa edelleen välttää numerolla alkavia id-attribuutteja. id-attribuutissa ei saa olla muita erikoismerkkejä kuin alaviiva(_), väliviiva(-) ja piste(.). Välilyöntiä ei saa olla.
- Pura paketti W:\TIEA2120-hakemiston alihakemistoon ohjaus2.
- Luo haluamallasi editorilla nanonano.js-tiedosto samaan kansioon tallentamasi dokumenttipohjan kanssa.
- Avaa editoriin ja Firefox-selaimeen Nanonanon kotisivu index.html.
- Linkitä nanonano.js HTML-dokumentin
head
-osaanscript
-elementillä.<script src="nanonano.js"></script>
- Kirjoita
nanonano.js
-tiedostoon ensin yksinkertaisin mahdollinen Javascript-ohjelma:"use strict"; alert("Hello world");
- Lataa sivu uudelleen selaimessa. Jos kaikki meni ok niin näytölle pitäisi ilmestyä dialogi jossa lukee Hello world. Huom! Sivusi on oltava WWW-palvelimella (users.jyu.fi), jotta kaikki varmasti toimii kuten on tarkoitus.
- Dialogi ilmestyy jo ennen sivun sisältöä joten korjataan ohjelmaa
sen verran, että sivu ilmestyy ensin ja vasta sitten dialogi. Siirrä
alert
-funktiokutsu window-luokanload
-tapahtumankäsittelijäfunktioon."use strict"; // estää pahimpia virheitä // liitetään oma funktio window.load-tapahtumaan, joka tapahtuu sitten kun koko html-dokumentti ja siihen liittyvät // kuvat ja muut tiedostot on ladattu window.addEventListener("load", function() { alert("Hello world"); });
- Kokeile ladata sivu uudelleen (reload). Nyt dialogi ilmestyy vasta dokumentin latauduttua.
- Kommentoi (// tai /* */) alert-komento pois ennen seuraavia tehtäviä. Älä koskaan käytä alert-funktiota esim. virheilmoituksiin.
Otsikoiden numerointi
Muutellaan elementtien sisältöä
- Lisää uusi funktio numeroi ja kutsu tätä window-objektin
load
-tapahtuman käsittelijäfunktiossa. - Hae numeroi-funktiossa kaikki
h2
-elementit sopivan nimiseen muuttujaan. Tämä onnistuu document.getElementsByTagName-metodilla. getElementsByTagName-funktio palauttaa aina NodeList-tyyppisen listan riippumatta siitä montako alkiota löytyi.NodeList toimii käytännössä kuten taulukko, mutta on ns. live eli DOM-puuhun mahdollisesti tehdyt muutokset heijastuvat suoraan kyseisen listan elementteihin.
document.getElementsByTagName etsii aina koko DOM-puusta. Elementti.getElementsByTagName etsii aina kyseisen elementin alla olevasta dokumenttihaarasta. - Haetaan h2-elementtien tekstit. Yritä tulostaa kaikkien h2-elementtien
tekstit konsoliin
- Voit käydä NodeListin läpi for..of-silmukalla
- Yksittäisen h2-elementin NodeListista saat item-metodilla tai []-operaattorilla aivan kuten taulukosta.
- Kokeile miten h2-elementin textContent-ominaisuus
toimii. Sillä saat suoraan jonkun elementin koko tekstisisällön eikä
tarvitse välittää erillisistä TextNodeista
Textnode?
- Kunkin elementin sisällä oleva teksti muodostaa oikeasti oman TextNode-tyypin olionsa. TextNodeja voi olla myös useita
- Textnoden saat tässä tapauksessa h2-elementin firstChild-attribuutista.
- Varsinaisen tekstin (String-tyyppiä) saa nodeValue-attribuutista. Huomaa, että nodeValue antaa tekstisisällön vain textNode-tyypeiltä.
- Yleensä riittää textContent-ominaisuuden käyttäminen eikä erillisistä textnodeista tarvi välittää
- textContent-attribuuttiin voi myös asettaa arvoja. Lisää sivulle näkyviin kunkin nykyisen otsikon eteen vielä otsikkonumerointi alkaen numerosta 1. Tarvitset tähän erillisen laskurimuuttujan.
- Miksi et voi kutsua numeroi()-funktiota muualta kuin load-tapahtuman sisältä? Kokeile laittaa kutsu load-tapahtuman ulkopuolelle. Mitä tapahtuu (tai ei tapahdu)? Miksi?
Elementtien lisäys
Lisätään elementtejä sivulle dynaamisesti DOM-rajapinnan avulla.
Linkit tekstiksi linkkilistaan
- Luo uusi funktio linkit ja kutsu sitä windowin
load
-käsittelijäfunktiossa, jonka loit jo aiemmin. - Merkitse id-ominaisuudella dokumentin lopussa oleva
ul
-elementti. - Hae linkit-funktiossa id:llä varustettu ul johonkin muuttujaan.
- Pyydä kaikkia tämän elementin alla olevia
a
-elementtejä getElementsByTagName-metodilla. - Tee silmukka ja hae kunkin
a
-elementin http-osoitehref
-ominaisuudesta ja tulosta se konsoliin. Tämä onnistuu suoraan href-ominaisuudesta (javascript-objektin property) tai getAttribute-metodilla, joka palauttaa HTML-elementin attribuutin arvon.Seuraavassa on demonstroitu miten käytetään javascript-objektin propertyja ja toisaalta html-elementin attribuutteja. Useimmilla html-attribuuteilla on olemassa vastaava javascript-objektin property, mutta ei kaikilla. Kannattaa aina tarkistaa MDN:n sivulta, että mitä propertyja on kullakin DOM-objektilla tarjolla. Omia propertyjahan saa myös vapaasti keksiä.
document.forms[1][1]; <input type="text" value=""> document.forms[1][1].type; "text" document.forms[1][1].value; "" document.forms[1][1].getAttribute("text"); // olematon ominaisuus on null null document.forms[1][1].getAttribute("type"); "text" document.forms[1][1].getAttribute("value"); "" document.forms[1][1].type = "number"; "number" document.forms[1][1].type; "number" document.forms[1][1].getAttribute("type"); "number" document.forms[1][1].setAttribute("type","email"); undefined document.forms[1][1].getAttribute("type"); "email" document.forms[1][1].type; "email" document.forms[1][1].value = 1; 1 document.forms[1][1].value; // value on AINA merkkijono "1" document.forms[1][1].getAttribute("value"); // hupsista, tämä ei kuitenkaan muutu, koska alkuperäinen sisältö on kentän oletusarvo "" document.forms[1][1].setAttribute("value","-"); // asettaa oletusarvon eikä varsinaista arvoa undefined document.forms[1][1].value; // arvo on edelleen se mitä on value-propertyyn asetettu "1" document.forms[1][1].getAttribute("value"); // kentän oletusarvo on muuttunut "-"
Huomaa kuinka input-elementin value-attribuutti toimii poikkeavasti.
JavaScript: What's the difference between HTML attribute and DOM property? - Luo osoitteesta uusi tekstisolmu createTextNode-metodilla li-elementin sisään (ei a-elementin). Miksi et voi tässä käyttää textContent-ominaisuutta? Voit kokeilla toteuttaa tätä textContentin avulla, niin huomaat miksi se ei onnistu.
- Lisää luomasi textnode kyseisen a-elementin sisältävän li-elementin loppuun. Tarvitset parentNode-attribuuttia ja appendChild-metodia. Sinun pitää myös keksiä miten saat a-elementin kautta käsiisi li-elementin. Esim. parentNode.
- Kokeile toimiiko skripti. Ulkoasua voi parantaa lisäämällä tekstin alkuun vielä yhden välilyönnin. Koodin uudelleenkäytettävyyttä voi vielä parantaa siten, että funktion parametriksi laitetaan id, tämä id haetaan dokumentista ja tämän jälkeläisinä olevat linkit "tekstitetään".
Tapahtumat
Tapahtumankäsittelijän lisääminen
- Luo johonkin kohtaan dokumenttia painike
button
-elementillä. Anna painikkeelle jokinid
-arvo ja näkyvä teksti Hello world. Älä laita painiketta form-elementin sisään. - Luo uusi funktio varoitus.
Tapahtumankäsittelijänä toimivat funktiot ottavat aina yhden parametrin, jonka
nimi on yleensä e.
function varoitus(e) { console.log("Hello world!") }
- Lisää onload-tapahtuman käsittelijään koodi, joka hakee painikkeen väliaikaismuuttujaan. Tämä onnistuu
document
-olion getElementById-metodilla. - Aseta painikkeelle
click
-tapahtuman käsittelijäksi funktio varoitus addEventListener-metodilla.painike.addEventListener("click", varoitus);
- Testaa painikkeen toimintaa.
- Muuta ohjelman toimintaa siten, että Hello world ilmestyy vain tuplaklikkauksesta (kts. HTML:n tapahtumat).
- Muuta tuplaklikkauksen käsittely takaisin tavalliseksi click-tapahtumaksi.
Siirrä painikkeesi lomakkeen (form-elementti) sisään. Miten ohjelmasi toimii nyt?
form action=""> </form>
- Lomakkeen sisällä oleva painike toimii lomakkeen lähetyspainikkeen
(submit-painike). Jos painikkeen tyypiksi määrää "button", niin painike ei
lähetä lomaketta:
submit-painike voi siis olla jompi kumpi seuraavista:<button type="button">Tavallinen painike</button>
Oletuksena lomakkeen sisältö lähetetään www-palvelimelle osoitteeseen, joka on ilmoitettu form-elementin action-ominaisuudessa, jossa on ohjelma, joka käsittelee lomakkeen tiedot. Tällä kurssilla ei käsitellä www-palvelimeen liittyviä ohjelmia. Käytännössä selain vain lataa sivusi uudelleen.<button>submit-painike</button> <label><input type="submit" />submit-painike</label>
- Jos halutaan javascriptilla käsitellä lomake, niin täytyy estää
lomakkeen lähettäminen
www-palvelimelle. Tämä tehdään preventDefault-metodilla
lomakkeen submit-tapahtumankäsittelijänä toimivassa funktiossa:
Samalla metodilla voidaan estää minkä tahansa tapahtuman oletustoiminta. Useimmiten estetään lomakkeen submit-tapahtuma, tai linkin tai buttonin click-tapahtuma.document.forms[0].addEventListener("submit", varoitus); function varoitus(e) { e.preventDefault(); ... }
- Lomakkeen sisällä oleva painike toimii lomakkeen lähetyspainikkeen
(submit-painike). Jos painikkeen tyypiksi määrää "button", niin painike ei
lähetä lomaketta:
Menu
Tehdään yksinkertainen menu, jossa alakohtia voidaan piilottaa. Tässä tarvitaan tapahtumankäsittelyä.
- Luo dokumentin alkuun HTML:ää kirjoittamalla kaksitasoinen sisäkkäinen lista, jossa on pääkohdat Artikkelit, Harkat ja Linkit ja näiden alle pari listakohtaa, joiden
sisällä on linkit. Kts. edellä oleva kuva. Lisää näiden listakohtien eteen
img
-elementillä minus.jpg-kuva merkitsemään avattua menukohtaa. - Merkitse ulompi lista sopivalla id-attribuutilla.
- Luo nanonano.js-tiedostoon uusi funktio muuta_nakyvyys ja aseta funktion ensimmäisen parametrin nimeksi e tai event.
- Hae window.onload-tapahtumankäsittelijässä edellä määrittelemälläsi id:llä varustettu
ul
-elementti johonkin muuttujaan document.getElementById-funktiolla. Tämä funktio palauttaa aina yhden elementin. - Pyydä kaikkia tämän ul-elementin alla olevia
img
-elementtejä getElementsByTagName-metodilla. - Käy silmukalla img-elementtilista läpi ja lisää kunkin elementin
click
-tapahtuman käsittelijäksi funktio muuta_nakyvyys.elementti.addEventListener("click", muuta_nakyvyys);
- Selvitä muuta_nakyvyys-funktion alussa mikä elementti on aiheuttanut tapahtuman.
Tämä selviää muuta_nakyvyys-funktiolle parametrina tulevan
event
-olion target-attribuutista. Voit esim. kokeilla mitä tekee console.log(event.target.nodeName) tai katsoa debuggerilla mitä event-parametri pitää sisällään. - Muuta kuvan (event.target) src-attribuutin arvoksi plus.jpg. Kts. setAttribute. Kokeile selaimella.
- Lisää logiikkaa ja aseta src-attribuutiksi plus.jpg jos ennen oli minus.jpg, muutoin toisinpäin. Muuta myös alt-attribuutin tekstiksi Auki/Kiinni tilan mukaan. Testaa toimintaa.
- Tutki Inspectorilla millainen puu sisäkkäisestä listasta muodostuu.
- Selvitä, miten pääset käsiksi tapahtuman aiheuttanutta
img
-elementtiä seuraavaan (sibling)ul
-elementtiin. Tapoja on monia, voit joutua käyttämään esim. joitain seuraavista attribuuteista/metodeista: children, parentNode, childNodes, nextSibling, nextElementSibling, nodeName ja getElementsByTagName (kts. DOM - Element). - Piilota sisempi ul-elementti samalla kun asetat src-attribuutin arvoksi plus.jpg, muutoin näytä elementti.
- Lisää CSS-tiedostoon luokka, jossa on ominaisuus
display: none;
. Käytä tätä luokkaa javascript-koodissasi.piilota { display: none; }
- Elementin näkyvyyttä voi muuttaa esimerkiksi className-attribuuttia muokkaamalla. Voit classNamen avulla ottaa elementissä käyttöön haluamasi css-tyylin.
- Elementin saa näkyväksi poistamalla
class
-attribuutin. Tämä onnistuu removeAttribute-metodilla.
- Lisää CSS-tiedostoon luokka, jossa on ominaisuus
- Poista vielä navigointipalkin uloimman listan alkioiden pallukat. Tämä onnistuu
helpoiten manipuloimalla näiden elementtien css-ominaisuuksia.
Vinkki: käy silmukalla läpi navigointipalkin lapsielement (childNodes) ja tee muutoksia niille, joiden nodeName on LI. getElementsByTagName-metodia voit myös käyttää, mutta tässä tulee ongelma. Hoksaatko mikä?elementti.style.display = "block";
- Voit vielä liu'uttaa (float: right;) koko listan sivun oikeaan laitaan.
Yhteenlaskupeli
Tehdään Nanonanon sivuille päässälaskun harjoitteluun pieni yhteenlaskutehtäviä generoiva peli.
- Lisää
h2
-otsikko ja sen allediv
-lohko, jonkaid
-ominaisuuden arvo on laskut. Lisää myös painike Tarkista, jolla on myös omaid
-ominaisuuden arvonsa. Huom. id ei saa alkaa numerolla. - Tee funktio luo_lasku, joka palauttaa p-elementin, joka
sisältää arvotun laskutehtävän tekstinä ja tekstilaatikon vastaukselle.
Tuotettu HTML-koodi on muotoa:
<p>LUKU1 + LUKU2 = <input type="number" size="3" /></p>
- Arvo tekstien LUKU1 ja LUKU2 tilalle jotkin luvut väliltä 0-99. Tämä onnistuu Math.random-metodilla.
- Tallenna arpomasi luvut luomasi input-elementin yhteyteen käyttämällä omia
propertyja. Esim. luku1 ja luku2.
input.luku1 = LUKU1;
- Tee funktio luo_laskut, joka luo kymmenen laskua silmukassa ja
lisää ne laskut-div-lohkon sisälle. Kutsu luo_lasku-funktiota
silmukassa. Lisää luo_laskut-funktiokutsu
window.onload
-tapahtuman käsittelijään. - Tee CSS-tiedostoon uusi luokka .virhe, joka muuttaa elementin taustavärin punaiseksi ja tekstin värin mustaksi.
- Tee funktio tarkista_laskut. Lisää tämä funktio
window.onload-tapahtuman käsittelijässä tarkista-napin
click
-tapahtuman käsittelijäksi. - Käy tarkista_laskut-funktiossa kaikki laskurivit läpi silmukassa ja tarkista
ovatko kaikki laskut oikein.
- Summaa luvut yhteen ja vertaile onko luku sama kuin tekstilaatikossa.
- Vertailua varten tekstikenttään syötetty luku on muutettava integer-tyyppiseksi parseInt- ja Number-funktioilla. Toteuta tavalla, jossa esim. "100a" ei kelpaa vaikka summa olisi 100.
- Jos laskettu luku ei ole oikein, niin laita tekstilaatikon
class
-attribuuttiin arvo virhe. Tätä vastaavaa property onclassName
, koskaclass
on javascriptissa varattu sana. - Jos rivi on kunnossa, niin poista
class
-attribuutti - Jos kaikki rivit ovat ok, niin ilmoita painikkeen alapuolella sopivalla tekstillä pelin läpipääsystä.
- Testaa "pelin" toimintaa.
Jos aikaa jäi, niin kokeile vielä toteuttaa uuden pelin arpominen ja vastaamiseen kuluneen ajan laskeminen pelin uudelleenaloituksesta hyväksyttyyn tarkistukseen.
Käyttäjien kommentit