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 DOM-rajapintaa. Älä käytä valmiita Javascript-kirjastoja.
- Avaa avuksesi
- DOM
- DOM ja tapahtumankäsittely
- Lomakkeet
- DOM Reference — MDN
- Jos HTML:n ja CSS:n perusteet eivät ole muistissa, 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 eli polyglot-dokumentti.
Xhtml-dokumenttia muokatessa on muistettava noudattaa XML-kielioppia, joka on tiukempi kuin HTML5-kielen löperö standardi sallii. Esimerkiksi MDN:n sivuilla on joitakin esimerkkejä kirjoitettu XML-epäyhteensopivasti, eivätkä ne suoraan kopioituna toimi xhtml-dokumentissa. Pyrimme koko kurssin ajan kirjoittamaan XHTML5-yhteensopivaa eli polyglot-html-koodia, joka tarkoittaa, että sama dokumentti toimii sekä text/html- että application/xhtml+xml-mediatyypillä.
Mitä tarkoittaa mediatyyppi?
Mediatyyppi tarkoittaa sitä, millaiseksi dokumentiksi www-palvelin väittää siltä lataamaamme tiedostoa. Se voi olla html-dokumentti, kuva, video, pdf-tiedosto ja niin edelleen. Windows-maailmassa on opittu, että tiedoston pääte määrittää tiedoston tyypin. Webbimaailmassa www-palvelin tai sillä 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. Onko kuva esimerkiksi tarkoitettu esitettäväksi www-sivun osana (image/jpeg) 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 mediatyyppi on text/html. Selain jäsentää tämän tyypin sisällön löysästi eli ei herjaa virheistä vaan korjailee puutteet 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ä kuulostaa tavallaan hyvältä, mutta oikeasti se aiheuttaa ongelmia. Missä kohtaa sivu on rakenteeltaan niin pahasti rikki, etteivät selaimet enää osaa sitä korjata? Emme tiedä. Jokainen selain voi yrittää korjata rakennetta eri tavalla, emmekä voi tietää mikä rikkoo minkäkin selaimen. Jos suomen kieltä 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 jäsentää xml-jäsentimellä. Se ei yritä korjata yhtään virhettä vaan heittää heti pyyhkeen kehään, jos rakenne on rikkinäinen. Käyttämällä .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. Esimerkiksi seuraava on tällä kurssilla ehdottomasti kiellettyä:
elementti.innerHTML = '<div class="foo">...<p>...</div>';XHTML-tilassa tuo heittää poikkeuksen, koska fragmentti ei ole hyvinmuodostunut (p-elementin lopetustagi puuttuu): sivu ei kaadu, mutta koodin suoritus keskeytyy siihen. Tavallinen html-jäsennin sallisi saman vaikka rakenne on rikki. Jonkun mielestä edellinen näyttää varmasti kätevältä ja aikaa säästävältä. Kyllä, tällä tavalla voisi joissain tilanteissa säästää koodirivejä. Tarkemmin ajateltuna se 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, ettei käyttäjän syötteiden avulla ujuteta dokumenttiin vaarallista koodia — joku syöttää lomakkeelle script-elementin ja kirjoittaa sen sisään mitä tahansa? 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ää. Muista kirjoittaa seuraavat elementit xhtml-dokumenttiin tässä annetussa muodossa:
<img src="..." alt="..." /> <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, XHTML5 ei salli.
Lisäksi muista id-attribuuteista (esim.
<input id="foobar" ... />) seuraavat asiat:- Jokaisen id-attribuutin arvon on oltava uniikki. Samassa dokumentissa ei voi olla kahta samaa arvoa. Jos näin on, dokumentti on virheellinen ja getElementById palauttaa vain ensimmäisen osuman — loput jäävät saavuttamatta.
- HTML5 sallii id-attribuutin arvoksi minkä tahansa merkkijonon, jossa ei ole välilyöntejä. Käytännössä kannattaa silti rajoittua kirjaimiin, numeroihin, alaviivaan ja väliviivaan, äläkä aloita arvoa numerolla. Muuten arvoa ei voi käyttää css-valitsimessa ilman suojauksia, ja vanhemmat standardit kielsivät sen kokonaan.
- 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ä. Huomaa defer-attribuutti: se kertoo selaimelle, että skripti suoritetaan vasta kun koko dokumentti on jäsennetty. XHTML:ssä arvo kirjoitetaan muodossa defer="defer", koska XML:ssä ei ole totuusarvoattribuutteja.<script src="nanonano.js" defer="defer"></script> - Kirjoita
nanonano.js-tiedostoon ensin yksinkertaisin mahdollinen Javascript-ohjelma:"use strict"; alert("Hello world"); - Lataa sivu uudelleen selaimessa. Jos kaikki meni ok, 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.
-
Muuta ohjelmaa niin, että koodi käynnistyy vasta kun sivu on latautunut. Siirrä
alert-funktiokutsuload-tapahtuman käsittelijäfunktioon."use strict"; // estää pahimpia virheitä // liitetään oma funktio window-objektin 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"); });Miksi tämä, jos skriptissä on jo defer? defer riittäisi tähän yksinään: sekin takaa, että DOM-puu on valmis ennen koodin suoritusta. Käytämme silti load-tapahtumaa, koska se on varmistus. Jos joku muu muokkaa html-rakennetta myöhemmin eikä säilytä defer-attribuuttia — tai siirtää skriptin toiseen kohtaan sivua — koodi ei silti hajoa. Ilman kumpaakaan
document.getElementByIdpalauttaisi null, koska elementtejä ei olisi vielä olemassa.Ero näiden kahden välillä on se, että defer ja DOMContentLoaded odottavat vain DOM-puun valmistumista, kun taas load odottaa lisäksi kuvat, tyylitiedostot ja muut resurssit. Tässä ohjauksessa käsitellään myös kuvia, joten load on luonteva valinta. Katso Skriptien lataaminen.
- Kokeile ladata sivu uudelleen. Nyt dialogi ilmestyy vasta dokumentin latauduttua.
- Kommentoi (// tai /* */) alert-komento pois ennen seuraavia tehtäviä. Älä koskaan käytä alert-funktiota esimerkiksi virheilmoituksiin: se pysäyttää koko sivun, sitä ei voi tyylitellä ja selain voi estää sen kokonaan. Käytä kehitysvaiheessa console.log-funktiota ja käyttäjälle näytettäviin viesteihin sivun omia elementtejä.
Otsikoiden numerointi
Muutellaan elementtien sisältöä.
Mitä ollaan tekemässä
Sivun otsikot ovat aluksi tällaisia:
<h2>Tietoja minusta</h2>
<h2>Opiskeluni</h2>
<h2>Harrastukseni</h2>
Ohjelman suorituksen jälkeen niiden pitäisi olla tällaisia:
<h2>1. Tietoja minusta</h2>
<h2>2. Opiskeluni</h2>
<h2>3. Harrastukseni</h2>
Rakenne ei siis muutu lainkaan — vain otsikoiden tekstisisältö muuttuu. Elementtejä ei luoda eikä poisteta, joten koko tehtävä hoituu textContent-ominaisuudella.
- Lisää uusi funktio
numeroija kutsu sitä window-objektinload-tapahtuman käsittelijäfunktiossa. -
Hae
numeroi-funktiossa kaikkih2-elementit sopivan nimiseen muuttujaan. Tämä onnistuu document.getElementsByTagName-metodilla.getElementsByTagName palauttaa aina HTMLCollection-tyyppisen kokoelman riippumatta siitä, montako alkiota löytyi. Kokoelma on elävä eli DOM-puuhun tehdyt muutokset heijastuvat siihen välittömästi.
HTMLCollection ei ole taulukko, vaikka siinä on length ja sitä voi indeksoida hakasulkeilla. Siinä ei ole forEach- eikä map-metodia. Jos tarvitset taulukon metodeja, muunna kokoelma taulukoksi:
Array.from(kokoelma)tai[...kokoelma].document.getElementsByTagName etsii aina koko DOM-puusta, elementti.getElementsByTagName vain kyseisen elementin jälkeläisistä.
- Haetaan h2-elementtien tekstit. Yritä tulostaa kaikkien h2-elementtien tekstit
konsoliin.
- Voit käydä kokoelman läpi for..of-silmukalla.
- Yksittäisen h2-elementin saat kokoelmasta item-metodilla tai []-operaattorilla aivan kuten taulukosta.
- Kokeile, miten h2-elementin
textContent-ominaisuus
toimii. Sillä saat suoraan elementin koko tekstisisällön eikä tarvitse välittää
erillisistä tekstisolmuista.
Tekstisolmu?
- Kunkin elementin sisällä oleva teksti muodostaa oikeasti oman Text-tyypin olionsa. Tekstisolmuja voi olla myös useita.
- Tekstisolmun saat tässä tapauksessa h2-elementin firstChild-ominaisuudesta.
- Varsinaisen tekstin saa nodeValue-ominaisuudesta. Huomaa, että nodeValue antaa tekstisisällön vain tekstisolmuilta.
- Yleensä riittää textContent-ominaisuuden käyttäminen eikä erillisistä tekstisolmuista tarvitse välittää.
- textContent-ominaisuuteen voi myös asettaa arvoja. Lisää sivulle näkyviin kunkin nykyisen otsikon eteen otsikkonumerointi alkaen numerosta 1. Tarvitset tähän erillisen laskurimuuttujan tai entries()-metodin.
- Miksi et voi kutsua
numeroi()-funktiota muualta kuin load-tapahtuman käsittelijästä? Kokeile laittaa kutsu käsittelijän ulkopuolelle. Mitä tapahtuu — tai ei tapahdu? Miksi?
Elementtien lisäys
Lisätään elementtejä sivulle dynaamisesti DOM-rajapinnan avulla.
Linkit tekstiksi linkkilistaan

Mitä ollaan tekemässä
Linkkilista on aluksi tällainen:
<ul id="linkit">
<li><a href="https://appro.mit.jyu.fi/">Appro</a></li>
<li><a href="https://www.jyu.fi/">Jyväskylän yliopisto</a></li>
</ul>
Ohjelman suorituksen jälkeen jokaisen linkin perään on lisätty sen osoite tekstinä:
<ul id="linkit">
<li><a href="https://appro.mit.jyu.fi/">Appro</a> https://appro.mit.jyu.fi/</li>
<li><a href="https://www.jyu.fi/">Jyväskylän yliopisto</a> https://www.jyu.fi/</li>
</ul>
Huomaa, että teksti tulee li-elementin sisään a-elementin jälkeen — ei linkin sisään. Jos teksti menisi a-elementin sisään, siitä tulisi osa klikattavaa linkkiä.
Näin nuo kaksi rakennetta näyttävät selaimessa. Kokeile klikata linkkejä: jälkimmäisessä osoite on tavallista tekstiä eikä osa linkkiä.
Ennen:
Jälkeen:
- Appro https://appro.mit.jyu.fi/
- Jyväskylän yliopisto https://www.jyu.fi/
- Luo uusi funktio
linkitja kutsu sitä windowinload-käsittelijäfunktiossa, jonka loit jo aiemmin. - Merkitse id-attribuutilla dokumentin lopussa oleva
ul-elementti. - Hae
linkit-funktiossa tuo ul-elementti johonkin muuttujaan. -
Pyydä kaikkia tämän ul-elementin alla olevia
a-elementtejä getElementsByTagName-metodilla. elementti.getElementsByTagName() etsii aina kyseisen elementin jälkeläisistä.Toinen vaihtoehto on elementti.querySelectorAll(), joka hakee elementtejä css-valitsimen perusteella (vrt. viikkotehtävässä 1 tehdyt valitsimet). Huomaa ero: querySelectorAll palauttaa staattisen NodeListin, getElementsByTagName elävän HTMLCollectionin.
Kokeile hakea oikeat a-elementit ensin getElementsByTagName-metodilla ja yritä sen jälkeen tehdä sama querySelectorAll-metodilla.
-
Tee silmukka, hae kunkin
a-elementin osoite href-ominaisuudesta ja tulosta se konsoliin. Sama onnistuu myös getAttribute-metodilla, joka palauttaa HTML-elementin attribuutin arvon.Seuraavassa on demonstroitu, miten käytetään javascript-objektin ominaisuuksia ja toisaalta html-elementin attribuutteja. Useimmilla html-attribuuteilla on olemassa vastaava javascript-ominaisuus, mutta ei kaikilla. Kannattaa aina tarkistaa MDN:n sivulta, mitä ominaisuuksia kullakin DOM-objektilla on tarjolla. Omia ominaisuuksia 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 attribuutti 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 muutu: "" // attribuutti on kentän OLETUSARVO document.forms[1][1].setAttribute("value","-"); undefined document.forms[1][1].value; // arvo on edelleen se, mikä "1" // value-ominaisuuteen asetettiin document.forms[1][1].getAttribute("value"); // oletusarvo on muuttunut "-"Huomaa, kuinka input-elementin value toimii poikkeavasti: attribuutti on alkuarvo, ominaisuus on nykyinen arvo. Kts. Datan liittäminen elementtiin ja 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ämän sen avulla, niin huomaat miksi se ei onnistu.
- Lisää luomasi tekstisolmu kyseisen a-elementin sisältävän li-elementin loppuun. Tarvitset parentNode-ominaisuutta ja append- tai appendChild-metodia. Sinun pitää myös keksiä, miten saat a-elementin kautta käsiisi li-elementin.
- Kokeile, toimiiko skripti. Ulkoasua voi parantaa lisäämällä tekstin alkuun vielä yhden välilyönnin. Koodin uudelleenkäytettävyyttä voi parantaa siten, että funktion parametriksi laitetaan id, tämä id haetaan dokumentista ja sen jälkeläisinä olevat linkit "tekstitetään".
Tapahtumat
Tapahtumankäsittelijän lisääminen
- Luo johonkin kohtaan dokumenttia painike
button-elementillä. Anna painikkeelle jokin id-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!"); }Toimiva malli. Avaa konsoli, niin näet saman tulosteen myös siellä. Vasen painike reagoi tavalliseen klikkaukseen, oikea vain tuplaklikkaukseen. Tuloste kertoo myös tapahtuman tyypin e.type-ominaisuudesta. - Lisää load-tapahtuman käsittelijään koodi, joka hakee painikkeen
muuttujaan. Tämä onnistuu
document-olion getElementById-metodilla. Huomaa, että metodi palauttaa null, jos elementtiä ei löydy — ei esimerkiksi tyhjää kokoelmaa. Juuri siitä seuraa aloittelijan yleisin virheilmoitus "Cannot read properties of null". - Aseta painikkeelle
click-tapahtuman käsittelijäksi funktiovaroitusaddEventListener-metodilla. Huomaa, ettei funktion nimen perään tule sulkeita: annamme osoittimen funktioon, emme kutsu sitä.painike.addEventListener("click", varoitus); - Testaa painikkeen toimintaa.
- Muuta ohjelman toimintaa siten, että Hello world ilmestyy vain tuplaklikkauksesta. Kts. dblclick.
- 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ähetyspainikkeena. Jos
painikkeen tyypiksi määrää button, painike ei lähetä lomaketta:
Lähetyspainike voi siis olla jompikumpi seuraavista:<button type="button">Tavallinen painike</button>
Huomaa, ettei lähetyspainiketta kääritä label-elementtiin: teksti tulee button-elementin sisältä tai value-attribuutista.<button>Lähetä</button> <input type="submit" value="Lähetä" /> - Oletuksena lomakkeen sisältö lähetetään www-palvelimelle osoitteeseen, joka on ilmoitettu form-elementin action-attribuutissa. Tällä kurssilla ei käsitellä www-palvelimeen liittyviä ohjelmia; käytännössä selain vain lataa sivusi uudelleen.
-
Jos lomake halutaan käsitellä javascriptillä, täytyy estää sen lähettäminen www-palvelimelle. Tämä tehdään preventDefault-metodilla lomakkeen submit-tapahtuman käsittelijässä. Huomaa, että submit on lomakkeen tapahtuma, ei painikkeen:
document.forms[0].addEventListener("submit", kasittele); function kasittele(e) { e.preventDefault(); // lomakkeen käsittely tähän }Samalla metodilla voidaan estää minkä tahansa tapahtuman oletustoiminta. Useimmiten estetään lomakkeen submit-tapahtuma tai linkin click-tapahtuma.
- Lomakkeen sisällä oleva painike toimii lomakkeen lähetyspainikkeena. Jos
painikkeen tyypiksi määrää button, painike ei lähetä lomaketta:
Menu

Tehdään menu, jonka alakohtia voidaan avata ja sulkea. Tässä tarvitaan tapahtumankäsittelyä. Menu tehdään kahteen kertaan kahdella eri tavalla, jotta ero tulee näkyviin.
Mitä ollaan tekemässä
Kokeile ensin alla olevaa mallia. Näin menun on tarkoitus toimia: kolmiosta avautuu ja sulkeutuu alakohtien lista.
Artikkelit
Harkat
Linkit
Huomaa, ettei tuossa mallissa ole riviäkään javascriptiä. Se on tehty pelkillä details- ja summary-elementeillä, jotka osaavat avautua ja sulkeutua itsestään. Ne toimivat myös näppäimistöltä, ja ruudunlukija kertoo tilan automaattisesti.
Oikeassa projektissa tekisit menun juuri noin. Tässä ohjauksessa se kuitenkin rakennetaan javascriptillä, koska tarkoitus on harjoitella DOM-rajapintaa ja tapahtumankäsittelyä — ei siksi, että se olisi paras tapa tehdä avattava lista. Tämä kannattaa muistaa: ennen kuin kirjoittaa javascriptiä, on hyvä tarkistaa, osaako HTML jo tehdä saman.
Versio 1: käsittelijä jokaiselle painikkeelle
-
Luo dokumentin alkuun kaksitasoinen sisäkkäinen lista, jossa on pääkohdat Artikkelit, Harkat ja Linkit ja näiden alla pari listakohtaa, joiden sisällä on linkit.
Laita jokaisen pääkohdan eteen painike, jonka sisällä on minus.jpg-kuva merkitsemään avattua menukohtaa:
<ul id="navi"> <li> <button type="button" aria-expanded="true"> <img src="minus.jpg" alt="" /> </button> Artikkelit <ul> <li><a href="">IT ja tietoliikenne</a></li> <li><a href="">Opiskelijaruokaa</a></li> </ul> </li> ... </ul>Miksi painike eikä pelkkä kuva? Klikattava kuva on käyttöliittymänä rikki: sitä ei voi käyttää näppäimistöllä, se ei ole osa sarkainkiertoa eikä ruudunlukija kerro sen olevan mitään muuta kuin kuva. button saa kaiken tämän ilmaiseksi. type="button" estää sitä lähettämästä lomaketta, jos menu joskus päätyy lomakkeen sisään.
aria-expanded-attribuutti kertoo ruudunlukijalle, onko kohta auki vai kiinni. Koska tila kerrotaan sillä, kuvan alt-teksti jätetään tyhjäksi: kuva on pelkkää koristetta, eikä sitä tarvitse lukea ääneen.
- Merkitse ulompi lista id-attribuutilla, esimerkiksi id="navi".
- Luo nanonano.js-tiedostoon uusi funktio
muuta_nakyvyys, joka ottaa parametrikseen painikkeen. - Hae load-tapahtuman käsittelijässä tuo lista muuttujaan document.getElementById-metodilla.
- Pyydä kaikkia listan alla olevia
button-elementtejä getElementsByTagName- tai querySelectorAll-metodilla. - Käy kokoelma silmukalla läpi ja lisää kunkin painikkeen
click-tapahtuman käsittelijä:for (const nappi of napit) { nappi.addEventListener("click", (e) => muuta_nakyvyys(e.currentTarget)); }currentTarget on elementti, jonka käsittelijää suoritetaan — tässä siis painike. target voisi olla painikkeen sisällä oleva kuva, koska klikkaus osuu siihen.
- Selvitä
muuta_nakyvyys-funktiossa, mikä alalista painikkeeseen liittyy. Painikkeen vanhempi on li-elementti, jonka sisältä alalista löytyy. Kokeile debuggerilla tai tulosta rakenne konsoliin. - Vaihda kuvan src-attribuutin arvoksi plus.jpg, jos
ennen oli minus.jpg, ja muutoin toisin päin. Kuvan saat painikkeen
sisältä esimerkiksi
nappi.querySelector("img")-kutsulla. -
Piilota alalista samalla kun asetat kuvaksi plus.jpg, ja näytä se muutoin.
- Lisää CSS-tiedostoon luokka:
.piilota { display: none; } - Luokkia käsitellään
classList-ominaisuudella.
toggle palauttaa true, jos luokka on kutsun jälkeen
paikallaan — eli jos lista on nyt piilossa:
const piilossa = alalista.classList.toggle("piilota"); - Vanhoissa esimerkeissä näkee className-ominaisuuden käyttöä ja luokan poistamista removeAttribute("class")-metodilla. Ne toimivat, mutta className on yksi merkkijono, jota joutuu pilkkomaan käsin, ja koko attribuutin poistaminen tuhoaa elementin muut luokat.
- Lisää CSS-tiedostoon luokka:
- Päivitä lopuksi aria-expanded-attribuutti vastaamaan tilaa, jotta
ruudunlukijan käyttäjä tietää mitä tapahtui:
nappi.setAttribute("aria-expanded", piilossa ? "false" : "true"); - Testaa hiirellä ja näppäimistöllä. Siirry sarkaimella painikkeeseen ja paina Enter tai välilyöntiä — menun pitäisi toimia myös näin.
- Tutki Inspectorilla, millainen puu sisäkkäisestä listasta muodostuu.

- Poista vielä navigointipalkin uloimman listan alkioiden pallukat.
Suoraviivaisin tapa on CSS-ominaisuus
list-style-type, joka javascriptissä kirjoitetaan camelCase-muodossa:elementti.style.listStyleType = "none";Myös
elementti.style.display = "block"poistaa pallukan, mutta sivuvaikutuksena: luettelomerkki syntyy arvosta display: list-item, ja kun display vaihdetaan, merkkikin katoaa. Tulos on sama, mutta list-style-type kertoo lukijalle mitä oikeasti tarkoitettiin.Vinkki: käy silmukalla läpi listan lapsielementit (children). Se sisältää vain elementit, joten childNodes-kokoelman tekstisolmuja ei tarvitse suodattaa pois.
Huomaa, että tämä kannattaisi oikeasti tehdä suoraan tyylitiedostossa. Tässä sitä harjoitellaan javascriptillä, jotta style-ominaisuuden käyttö tulee tutuksi.
- Voit vielä siirtää koko listan sivun oikeaan laitaan.
Versio 2: sama tehtävä delegoinnilla
Edellä lisäsit käsittelijän silmukassa jokaiseen painikkeeseen. Tehdään sama uudelleen toisella tavalla. Tapahtumat kuplivat eli etenevät kohteesta ylöspäin kohti dokumentin juurta, joten käsittelijän voi liittää yhteen yhteiseen esivanhempaan. Tätä kutsutaan delegoinniksi. Kts. Tapahtuman eteneminen DOM-puussa ja Delegointi.
- Kommentoi pois silmukka, jossa lisäsit käsittelijän jokaiselle painikkeelle.
- Lisää yksi ainoa käsittelijä uloimmalle
ul-elementille:navi.addEventListener("click", menua_klikattu); -
Kirjoita
menua_klikattu-funktio. Nyt e.currentTarget on aina koko lista, joten se ei kerro mitä klikattiin — e.target kertoo. Klikkaus voi osua mihin tahansa listan sisällä, myös linkkiin tai tekstiin, joten tarkista onko kohde painike:function menua_klikattu(e) { const nappi = e.target.closest("button"); if (nappi === null) { return; // klikkaus ei osunut painikkeeseen, ei tehdä mitään } muuta_nakyvyys(nappi); }closest etsii ylöspäin: se palauttaa lähimmän esivanhemman tai elementin itsensä, joka vastaa css-valitsinta — tai null, jos osumaa ei ole. Tämä on tässä välttämätöntä, koska klikkaus osuu useimmiten painikkeen sisällä olevaan kuvaan eikä painikkeeseen itseensä.
- Testaa, että menu toimii edelleen — myös näppäimistöltä.
-
Kokeile, mitä delegoinnilla voitti. Lisää sivulle painike, joka lisää menuun uuden kohdan ajon aikana:
<p><button type="button" id="lisaa_menukohta">Lisää menuun uusi kohta</button></p>Kirjoita painikkeelle käsittelijä, joka rakentaa uuden menukohdan DOM-rajapinnalla. Uusi elementti luodaan document.createElement-metodilla, ja se liitetään puuhun append-metodilla. Luotu elementti ei näy sivulla ennen kuin se liitetään:
function lisaa_menukohta() { // createElement luo elementin, mutta se on aluksi irrallaan dokumentista const kohta = document.createElement("li"); const nappi = document.createElement("button"); nappi.type = "button"; nappi.setAttribute("aria-expanded", "true"); const kuva = document.createElement("img"); kuva.src = "minus.jpg"; kuva.alt = ""; // append ottaa vastaan useita argumentteja kerralla ja hyväksyy myös // merkkijonoja, jotka lisätään TEKSTINÄ - ei html-koodina nappi.append(kuva); kohta.append(nappi, "Uusi kohta"); const alalista = document.createElement("ul"); const rivi = document.createElement("li"); rivi.textContent = "Alakohta"; alalista.append(rivi); kohta.append(alalista); // vasta tämä rivi saa kohdan näkyviin document.getElementById("navi").append(kohta); }Huomaa, ettei missään käytetä innerHTML-ominaisuutta. Rakenne kootaan elementti kerrallaan, jolloin DOM-rajapinta pitää huolen siitä, että lopputulos on ehjä. Tämä on sama tekniikka, jota tarvitset kohta yhteenlaskupelissä.
-
Paina painiketta ja klikkaa sitten uuden kohdan kuvaketta. Se toimii, vaikka sille ei koskaan lisätty käsittelijää — käsittelijä on listassa, ja tapahtuma kuplii sinne.
Kokeile samaa versiolla 1: palauta silmukka käyttöön, kommentoi delegoitu käsittelijä pois ja paina painiketta uudestaan. Nyt uusi kohta ei reagoi mitenkään, koska silmukka ehti käydä jo ennen sen luomista.
-
Kokeile lopuksi, mitä tapahtuu, jos etenemisen estää. Lisää
muuta_nakyvyys-funktion alkuun väliaikaisestie.stopPropagation();versiossa 1 ja katso konsolista, ettei listalle liitetty käsittelijä enää suoriudu. Poista rivi kokeilun jälkeen. Tämä on syy siihen, miksi stopPropagation-metodia pitää käyttää harkiten: se rikkoo muiden koodinpätkien delegoinnin, eikä vikaa ole helppo löytää.
Kumpi versio on parempi? Delegoitu. Käsittelijöitä tarvitaan yksi sadan sijaan, ja käsittely toimii myös elementeille, jotka lisätään sivulle vasta myöhemmin. Silmukkaversio on silti hyvä tuntea, koska sitä näkee paljon.
Yhteenlaskupeli

Tehdään Nanonanon sivuille päässälaskun harjoitteluun pieni yhteenlaskutehtäviä generoiva peli.
Mitä ollaan tekemässä
Kokeile alla olevaa mallia. Se on perusversio: kymmenen arvottua laskua ja Tarkista-painike. Laajennukset lisäävät tähän vielä vapaan rivimäärän ja rivien poistamisen.
Tämä malli rakennetaan javascriptillä. Jos tämä teksti jää näkyviin, skriptiä ei ladattu.
Yksi laskurivi on rakenteeltaan tällainen:
<p>07 + 42 = <input type="number" aria-label="07 plus 42" /></p>
Arvotut luvut eivät näy html-koodissa muuten kuin tekstinä. Ne talletetaan input-elementin omiin ominaisuuksiin, jolloin ne säilyvät oikeina numeroina eikä niitä tarvitse jäsentää takaisin tekstistä.
- Lisää
h2-otsikko ja sen allediv-lohko, jonka id-attribuutin arvo on laskut. Lisää myös painike Tarkista, jolla on oma id-arvonsa. Huom. id ei saa alkaa numerolla. - Tee funktio
luo_lasku, joka palauttaa p-elementin. Se sisältää arvotun laskutehtävän tekstinä ja tekstilaatikon vastaukselle. Tuotettu HTML-koodi on muotoa:<p>LUKU1 + LUKU2 = <input type="number" /></p>-
Elementit luodaan document.createElement-metodilla ja liitetään toisiinsa append-metodilla. Luotu elementti ei näy sivulla ennen kuin se liitetään dokumenttipuuhun:
const p = document.createElement("p"); const kentta = document.createElement("input"); kentta.type = "number"; p.textContent = `${luku1} + ${luku2} = `; p.append(kentta); return p;Älä käytä innerHTML-ominaisuutta. Se on tällä kurssilla kielletty, ja syyt on kerrottu Hello World -luvun alussa.
- Arvo tekstien LUKU1 ja LUKU2 tilalle jotkin luvut väliltä 0–99. Tämä onnistuu Math.random-funktiolla.
- Tallenna arpomasi luvut luomasi input-elementin yhteyteen käyttämällä omia
ominaisuuksia, esimerkiksi
input.luku1 = LUKU1;. Näin luvut pysyvät oikeina numeroina eikä niitä tarvitse lukea takaisin tekstistä. - Huom. size-attribuutti ei ole sallittu numerokentälle. Kentän leveys
säädetään tyylitiedostossa, esimerkiksi
#laskut input { width: 4em; }.
-
- Tee funktio
luo_laskut, joka luo kymmenen laskua silmukassa ja lisää ne laskut-div-lohkon sisälle. Kutsuluo_lasku-funktiota silmukassa. Lisääluo_laskut-funktiokutsu load-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ää se load-tapahtuman käsittelijässä Tarkista-painikkeenclick-tapahtuman käsittelijäksi. - Käy
tarkista_laskut-funktiossa kaikki laskurivit läpi silmukassa ja tarkista, ovatko kaikki laskut oikein.- Summaa luvut yhteen ja vertaa, onko tulos sama kuin tekstilaatikossa.
-
Vertailua varten tekstikenttään syötetty arvo on muutettava numeroksi. Kentän value-ominaisuus on aina merkkijono, myös numerokentässä. Muunnokseen on kolme tapaa:
Number.parseInt(kentta.value, 10); // lukee alusta niin pitkälle kuin pystyy Number(kentta.value); // koko merkkijonon on kelvattava kentta.valueAsNumber; // vain numerokentillä (type="number")Toteuta tavalla, jossa esimerkiksi "100a" ei kelpaa vaikka summa olisi 100. Kokeile kaikkia kolmea ja katso, mitä ne palauttavat syötteillä "100", "100a" ja "".
Number.parseInt hyväksyy "100a"-syötteen ja palauttaa 100, joten se yksinään ei riitä. Number hylkää sen ja palauttaa NaN — mutta varo: Number("") on 0 eikä NaN. valueAsNumber on numerokentälle siistein: se palauttaa NaN, jos kenttä on tyhjä tai sisältö ei kelpaa luvuksi. Tarkista tulos Number.isNaN-metodilla.
- Jos laskettu luku ei ole oikein, lisää tekstilaatikolle luokka
virhe:
kentta.classList.add("virhe"); - Jos rivi on kunnossa, poista luokka
kentta.classList.remove("virhe")-kutsulla. Vielä lyhyemmin molemmat tapaukset hoituvat yhdellä rivillä:kentta.classList.toggle("virhe", !oikein). - Jos kaikki rivit ovat ok, ilmoita painikkeen alapuolella sopivalla tekstillä pelin läpipääsystä.
- Testaa pelin toimintaa.
Laajennus 1: vapaa määrä laskurivejä
Perusversio luo aina kymmenen laskua. Tehdään pelistä dynaaminen: käyttäjä saa itse päättää, montako riviä lisätään ja milloin.
- Lisää sivulle numerokenttä rivien määrälle ja painike, jolla rivit lisätään.
Anna kentälle järkevät rajat ja oletusarvo:
<p> <label>Lisättäviä rivejä <input type="number" id="maara" value="5" min="1" max="50" /> </label> <button type="button" id="lisaa">Lisää rivejä</button> </p> - Muuta
luo_laskut-funktiota niin, että se ottaa parametrikseen lisättävien rivien määrän. Funktio ei enää tyhjennä vanhoja rivejä vaan lisää uudet olemassa olevien perään. -
Lue lisättävä määrä kentästä. value on aina merkkijono, myös numerokentässä, joten muunna se luvuksi. Muista myös, ettei käyttäjän syötteeseen voi luottaa:
const kentta = document.getElementById("maara"); const maara = kentta.valueAsNumber; if (!Number.isInteger(maara) || maara < 1) { kentta.reportValidity(); // näyttää selaimen oman virheilmoituksen return; }valueAsNumber on NaN, jos kenttä on tyhjä tai sisältö ei kelpaa luvuksi. Sama onnistuu myös Number.parseInt- tai Number-funktiolla, mutta varo:
Number("")on nolla eikä NaN. - Liitä
lisaa-painikkeeseenclick-käsittelijä, joka kutsuuluo_laskut-funktiota kentästä luetulla määrällä. - Säilytä "Uusi peli" -painike, mutta muuta sen toimintaa: se tyhjentää kaikki rivit ja aloittaa alusta. Tyhjentämiseen sopii replaceChildren() ilman argumentteja.
-
Jos lisättäviä rivejä on paljon, kannattaa koota ne ensin irralliseen DocumentFragment-säiliöön ja liittää dokumenttiin kerralla. Silloin selain laskee sivun asettelun uudelleen kerran eikä viisikymmentä kertaa:
const fragmentti = document.createDocumentFragment(); for (let i = 0; i < maara; i++) { fragmentti.append(luo_lasku()); } laskut.append(fragmentti); - Testaa. Huomaa, ettei
tarkista_laskut-funktioon tarvitse koskea lainkaan: se käy läpi kaikki laskut-lohkon kentät riippumatta siitä, montako niitä on.
Laajennus 2: laskurivin poistaminen
Lisätään jokaisen rivin viereen painike, jolla rivin voi poistaa.
-
Muuta
luo_lasku-funktiota niin, että se lisää riville myös poistopainikkeen:const poista = document.createElement("button"); poista.type = "button"; poista.className = "poista"; poista.textContent = "×"; poista.setAttribute("aria-label", "Poista tämä rivi"); p.append(poista);Huomaa kolme asiaa. Käytä
button-elementtiä äläkä pelkkää ×-merkkiä tekstinä: nappi toimii myös näppäimistöltä ja ruudunlukija kertoo sen olevan painike. Anna type="button", muuten painike lähettäisi lomakkeen, jos rivit joskus siirretään lomakkeen sisään. Ja koska pelkkä × ei kerro ruudunlukijalle mitään, kirjoita aria-label.Jos kirjoitat ×-merkin suoraan HTML-koodiin, muista ettei × kelpaa XHTML-dokumentissa: XML tuntee vain viisi nimettyä merkkiviittausta. Käytä numeerista muotoa × tai kirjoita merkki sellaisenaan UTF-8-tiedostoon.
-
Käytä delegointia. Älä lisää käsittelijää jokaiseen poistopainikkeeseen erikseen — rivejä syntyy jatkuvasti lisää, eikä uusi rivi saisi käsittelijää lainkaan. Liitä yksi käsittelijä laskut-lohkolle:
const laskut = document.getElementById("laskut"); laskut.addEventListener("click", rivia_klikattu); function rivia_klikattu(e) { const nappi = e.target.closest("button.poista"); if (nappi === null) { return; // klikkaus ei osunut poistopainikkeeseen } // poistetaan koko rivi eli se p-elementti, jonka sisällä painike on nappi.closest("p").remove(); }Tässä closest-metodia käytetään kahdesti eri suuntiin ajatellen: ensin selvitetään osuiko klikkaus poistopainikkeeseen, sitten haetaan se rivi, johon painike kuuluu. remove() poistaa elementin itsensä — vanha tapa olisi ollut
rivi.parentNode.removeChild(rivi). - Testaa. Lisää rivejä, poista muutama väliltä ja tarkista laskut. Kaiken pitäisi
toimia ilman että
tarkista_laskut-funktiota muutetaan. -
Mieti reunatapaus: mitä tapahtuu, jos poistat kaikki rivit ja painat sitten Tarkista? Virheitä ei ole yhtään, joten peli onnittelee sinua läpipääsystä. Korjaa tämä.
- Jos haluat viimeistellä, tyhjennä tulosteksti aina kun rivejä lisätään tai poistetaan — muuten edellisen tarkistuksen tulos jää näkyviin, vaikka rivit ovat muuttuneet.
Datan hakeminen palvelimelta
Tähän asti kaikki sivun data on ollut kovakoodattuna joko html-koodissa tai javascript-tiedostossa. Oikeassa sovelluksessa data haetaan palvelimelta. Tässä tehtävässä kokeillaan sitä fetch-rajapinnalla.
Esimerkkidatana käytetään JSONPlaceholder-palvelua, joka tarjoaa testidataa ja sallii pyynnöt mistä tahansa lähteestä. Osoite https://jsonplaceholder.typicode.com/users palauttaa kymmenen käyttäjää JSON-muodossa:
[
{ "id": 1, "name": "Leanne Graham", "email": "Sincere@april.biz", ... },
{ "id": 2, "name": "Ervin Howell", "email": "Shanna@melissa.tv", ... },
...
]
Mitä ollaan tekemässä
Tämä malli rakennetaan javascriptillä. Jos tämä teksti jää näkyviin, skriptiä ei ladattu.
Tehtävä
- Lisää sivulle painike, tilarivi ja tyhjä lista:
<p><button type="button" id="hae">Hae käyttäjät</button></p> <p id="haku-tila"></p> <ul id="kayttajat"></ul> -
Tee
nanonano.js-tiedostoon uusi funktiohae_kayttajat. Huomaa async-sana: ilman sitä await ei ole käytettävissä.async function hae_kayttajat() { const vastaus = await fetch("https://jsonplaceholder.typicode.com/users"); const kayttajat = await vastaus.json(); console.log(kayttajat); }Liitä funktio painikkeen
click-tapahtumaan ja tarkista konsolista, että data tulee perille. -
Lisää tilatarkistus. Fetch ei hylkää lupausta HTTP-virheestä: vastaus 404 tai 500 näyttää koodille onnistuneelta. Tilanne on tarkistettava itse:
if (!vastaus.ok) { throw new Error(`HTTP-virhe ${vastaus.status}`); }Kokeile vaihtaa osoitteeksi .../userz ja katso, mitä tapahtuu ilman tarkistusta ja sen kanssa.
- Rakenna jokaisesta käyttäjästä
li-elementti ja lisää ne listaan. Käytä createElement- ja append-metodeja kuten yhteenlaskupelissä, ja kokoa rivit DocumentFragment-säiliöön ennen liittämistä. Tyhjennä lista replaceChildren()-metodilla, jottei sisältö kahdennu kun painiketta painetaan uudestaan. -
Näytä käyttäjälle mitä tapahtuu. Verkkopyyntö kestää, ja sivu ei saa näyttää jumittuneelta. Lukitse painike haun ajaksi ja kirjoita tilariville teksti:
nappi.disabled = true; tila.textContent = "Haetaan…"; -
Käsittele virheet. Kääri haku try-lohkoon ja näytä virheilmoitus tilarivillä — ei pelkästään konsolissa, jonne käyttäjä ei katso. Avaa painike takaisin finally-lohkossa, jotta se avautuu myös virhetilanteessa:
try { // haku ja listan rakentaminen } catch (virhe) { tila.textContent = `Haku epäonnistui: ${virhe.message}`; } finally { nappi.disabled = false; }Testaa: katkaise verkkoyhteys tai kirjoita osoitteeseen kirjoitusvirhe ja varmista, että painike avautuu ja virheilmoitus näkyy sivulla.
- Kokeile lopuksi, missä järjestyksessä asiat tapahtuvat. Lisää
console.log-kutsu funktion kutsun jälkeen ja katso, tulostuuko se ennen vai jälkeen datan.
Laajennus: käyttäjän postaukset
Jos aikaa jäi, tee listasta klikattava. Klikkaamalla nimeä haetaan kyseisen käyttäjän kirjoitukset osoitteesta https://jsonplaceholder.typicode.com/posts?userId=1 ja näytetään ne nimen alla.
- Tallenna käyttäjän tunnus riville omana ominaisuutena, kuten teit
yhteenlaskupelissä:
rivi.kayttajaId = kayttaja.id;. Näin tunnus säilyy numerona eikä sitä tarvitse lukea takaisin tekstistä. - Käytä delegointia. Rivit syntyvät vasta haun jälkeen, joten yksittäisiin riveihin ei kannata lisätä käsittelijöitä. Liitä yksi käsittelijä listalle ja selvitä klikattu rivi closest-metodilla — sama kuvio kuin menussa ja laskurivien poistossa.
- Hae postaukset samalla tavalla kuin käyttäjät ja lisää ne rivin sisään omaksi alalistakseen.
- Mieti, mitä pitäisi tapahtua jos samaa nimeä klikataan kahdesti. Yksinkertaisin ratkaisu on piilottaa jo haettu lista classList-metodilla eikä hakea dataa uudestaan.
XML-muotoinen data
Kaikki data ei tule JSON-muodossa. XML on yhä yleinen etenkin vanhemmissa rajapinnoissa ja RSS-syötteissä, ja sen käsittely on hyvä osata. Tässä pienessä tehtävässä haetaan Wikipedian hakutulokset XML-muodossa.
Wikipedian rajapinta on vapaasti käytettävissä ja sallii pyynnöt mistä tahansa lähteestä, kun osoitteeseen lisätään parametri origin=*. Ilman sitä selain estäisi vastauksen lukemisen — kokeile myös ilman, niin näet CORS-virheen konsolissa.
Tämä malli rakennetaan javascriptillä. Jos tämä teksti jää näkyviin, skriptiä ei ladattu.
-
Katso ensin, millaista dataa saat. Avaa seuraava osoite selaimessa ja tutki rakennetta ennen kuin kirjoitat riviäkään koodia:
https://fi.wikipedia.org/w/api.php?action=query&list=search&srsearch=javascript&srlimit=5&format=xml&origin=*Vastaus on jotakin tällaista. Huomaa, että hakutuloksen elementin nimi on p — se on MediaWikin oma nimi eikä liity HTML:n kappale-elementtiin, ja kiinnostava tieto on attribuuteissa eikä tekstisisällössä:
<api> <query> <search> <p ns="0" title="JavaScript" size="24601" wordcount="3020" /> <p ns="0" title="ECMAScript" size="5312" wordcount="620" /> </search> </query> </api> - Lisää sivulle tekstikenttä hakusanalle, painike ja tyhjä lista tuloksille. Muista label-elementti kentälle.
-
Kokoa osoitteen kyselyosa URLSearchParams-objektilla äläkä liimaamalla merkkijonoja yhteen. Se hoitaa erikoismerkkien koodauksen puolestasi — muuten hakusana "C++" tai ääkköset rikkoisivat osoitteen:
const parametrit = new URLSearchParams({ action: "query", list: "search", srsearch: hakusana, srlimit: "5", format: "xml", origin: "*" }); const osoite = `https://fi.wikipedia.org/w/api.php?${parametrit}`; -
Hae data fetch-funktiolla ja tarkista vastaus.ok kuten edellisessä tehtävässä. XML-datalle ei ole omaa metodia, joten runko luetaan tekstinä text()-metodilla. Sekin palauttaa lupauksen, joten tarvitaan await:
const vastaus = await fetch(osoite); if (!vastaus.ok) { throw new Error(`HTTP-virhe ${vastaus.status}`); } const teksti = await vastaus.text();Huomaa, että fetch palauttaa Response-olion eikä vastauksen sisältöä. Jos jäsentimelle annetaan vastaus-olio teksti-merkkijonon sijaan, olio muuttuu merkkijonoksi "[object Response]". Se ei ole XML:ää, joten jäsennys epäonnistuu, vaikka rajapinta palautti kelvollista dataa.
-
Jäsennä teksti DOMParser-luokalla. Tuloksena on tavallinen Document, jota käsitellään samoilla DOM-metodeilla kuin www-sivua:
const jasennin = new DOMParser(); const xml = jasennin.parseFromString(teksti, "application/xml");Toisen parametrin on oltava "application/xml". Arvolla "text/html" muotovirheet korjailtaisiin hiljaisesti.
-
Tarkista jäsennyksen onnistuminen. Tämä yllättää: DOMParser ei heitä poikkeusta virheellisestä XML:stä, vaan palauttaa dokumentin, jonka sisällä on parsererror-elementti. Tarkistus on tehtävä itse:
const virhe = xml.querySelector("parsererror"); if (virhe !== null) { throw new Error(`Virheellinen XML: ${virhe.textContent}`); } - Käy tulokset läpi valitsimella "search p" ja lue otsikko getAttribute("title")-metodilla. Rakenna jokaisesta li-elementti ja lisää ne listaan.
- Muista lataustila ja virheenkäsittely samalla tavalla kuin edellisessä tehtävässä: painike lukkoon haun ajaksi, virheilmoitus näkyviin sivulle ja painike auki finally-lohkossa.
-
Kokeile lopuksi kahta asiaa ja katso mitä konsolissa lukee:
- Poista osoitteesta origin=*. Pyyntö epäonnistuu, ja fetch hylkää lupauksen TypeError-virheellä — tilakoodia ei näy, koska koodi ei saa lukea vastauksesta mitään. Varsinainen syy löytyy vain konsolin CORS-virheilmoituksesta.
- Vaihda format=xml muotoon format=json mutta jätä jäsennys ennalleen. Mitä parsererror-tarkistus nyt tekee?
Vertaa lopuksi kahta tapaa: JSON-datalla riitti yksi json()-kutsu, XML vaati tekstin lukemisen, erillisen jäsentimen ja oman virhetarkistuksen. Tämä on yksi syy siihen, miksi uudet rajapinnat tarjoavat dataa JSON-muodossa.
Lopuksi: tarkista sivun rakenne
Ohjauksen alussa käytiin läpi, miksi html-rakenteen on pysyttävä ehjänä ja miksi kurssilla kirjoitetaan polyglot-dokumentteja. Nyt on aika tarkistaa, että javascript-koodisi on pitänyt lupauksensa.
Tämä on tärkeää juuri nyt, koska tavallinen validointi näkee vain staattisen lähdekoodin. Kaikki tässä ohjauksessa tuotettu — otsikoiden numerot, linkkien perään lisätyt osoitteet, laskurivit ja poistopainikkeet — syntyy vasta ajon aikana. Validaattori ei näe niistä mitään, ellei sille erikseen näytetä.
-
Aja sovellus siihen tilanteeseen, jonka haluat tarkistaa: lisää laskurivejä, poista muutama, avaa ja sulje menun kohtia ja hae käyttäjät palvelimelta.
-
Paina Ctrl+Alt+V. Dokumenttipohjaan on linkitetty validaattoriskripti, joka lähettää sivun nykyisen rakenteen W3C:n validaattorille. Tulokset ilmestyvät konsoliin.
-
Katso konsoliin myös htmlinspectorin huomiot. Pohja seuraa DOM-puuhun tehtyjä muutoksia ja tarkistaa rakenteen automaattisesti aina kun sivu muuttuu, joten virheet näkyvät heti kun ne syntyvät.
-
Voit tehdä saman myös käsin, jolloin näet tarkalleen mitä validaattorille lähetetään. Avaa Inspector, klikkaa html-elementtiä hiiren oikealla ja valitse Copy → Outer HTML. Liitä tulos validaattorin tekstikenttään. Sama onnistuu konsolista:
copy(document.documentElement.outerHTML);copy() on konsolin oma apufunktio, ei osa javascript-kieltä.
-
Kokeile rikkoa rakenne tahallasi ja katso, huomaako validointi sen. Lisää esimerkiksi
luo_lasku-funktioon rivi, joka liittää input-elementin sisään toisen elementin, tai luo elementti väärällä nimellä. Korjaa virhe kokeilun jälkeen. -
Tarkista lopuksi vielä itse index.xhtml-tiedosto avaamalla se Firefoxilla. Koska pääte on .xhtml, selain jäsentää sen XML-jäsentimellä ja pysähtyy heti ensimmäiseen muotovirheeseen. Tämä ei kerro javascriptin tuottamasta rakenteesta mitään, mutta se varmistaa, että kirjoittamasi html on kunnossa.
Lisätietoa: Työkalut ja Dynaamisesti tuotetun sivun tarkistaminen.
Jos aikaa jäi, kokeile vielä toteuttaa uuden pelin arpominen ja vastaamiseen kuluneen ajan laskeminen pelin uudelleenaloituksesta hyväksyttyyn tarkistukseen. Ajan mittaamiseen käytetään Date.now()- tai performance.now()-funktiota, ei new Date()-oliota.
Käyttäjien kommentit