Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.

Web-selainohjelmointi

Lukijalle

Tämä materiaali on tarkoitettu Helsingin yliopiston tietojenkäsittelytieteen laitoksen syksyn 2012 kurssille web-selainohjelmointi. Materiaalin kirjoittaja on Arto Vihavainen ja sen syntyyn ovat vaikuttaneet useat tahot, joista tärkein on Mikael Nousiainen. Iso kiitos kuuluu myös Kasper Hirvikoskelle. Kurssia web-selainohjelmointi on edeltänyt aiemmin järjestetty kurssi Digitaalisen Median tekniikat, jonka tämä kurssi korvaa. "DiMe"-kurssin viimeisten vetäjien Samuli Kaipiaisen ja Matti Paksulan henki kuitenkin elää kurssilla.

Materiaali päivittyy kurssin edetessä. Osa tehtävistä on osana materiaalia, osa taas linkkeinä omille sivuilleen. Tehtävien lisäksi materiaali sisältää kysymysmerkillä merkittyjä pohdi-kohtia, joissa pääsee pohtimaan juuri tutuksi tullutta asiaa esimerkin kautta. Lampuilla merkityt kohdat taas sisältävät mm. arvokkaita vinkkejä erilaisista työkaluista.

Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen ja muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen. Esimerkkien ja erityisesti omien kokeilujen tekeminen on parhaita tapoja sisäistää luettua. Muista että copy-paste ei ole oppimisen kannalta yhtä tehokasta kuin itse kirjoittaminen.

Pyri tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä saat ohjausaikoina neuvoja tehtävien tekemiseen.

Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä materiaali ei sisällä kaikkea oleellista web-selainohjelmointiin liittyvää. Tällä hetkellä ei ole oikeastaan mitään kirjaa josta löytyisi kaikki oleellinen. Joudut kurssin aikana ja urallasi etsimään tietoa myös omatoimisesti. Harjoitukset ja materiaali sisältävät jo jonkin verran ohjeita, mistä suunnista tietoa kannattaa lähteä hakemaan.

Jos (ja kun) materiaalista löytyy esimerkiksi kirjoitusvirheitä, raportoikaa niistä esimerkiksi kurssikanavalla. Kiitos bugien ja ongelmien korjauksesta kuuluu jo nyt seuraaville nimimerkeille: pro_, gleant, BiQ, Absor, Rase, jombo, BearGrylls, Marko, _jumi_, Semilia ja mluukkai.

Miksi selainohjelmointia?

Ensimmäisellä luennolla..

Tools of the trade

Käydään läpi pikaisesti oleellisia työkaluja.

Web-selainten tarjoamat kehittäjien työkalut

Ohjelmien debuggaaminen on oleellinen taito. Selainohjelmistot pyörivät selaimessa, joten luonnollinen paikka niiden debuggaamiseen on selaimessa. Esimerkiksi google chrome ja mozilla firefox tarjoavat debuggausympäristöt, joilla voi tutkia sivuja. Debuggausympäristöt aukeavat yleensä nappia f12-painamalla. Oleellisin osio lienee konsoli, mistä näkee esimerkiksi JavaScript-suorituksessa tapahtuvat virheviestit.

Omiin sovelluksiin voi myös lisätä debuggauskomentoja. Esimerkiksi komento console.log("viesti"); osana JavaScript-kutsua lisää konsoliin viestin "viesti". Sovelluksen debuggaus viestien avulla on hyvä aloituskohta rikkinäisten sovellusten korjaamiselle..

Tutustu Chrome Developer Toolseihin osoitteessa https://developers.google.com/chrome-developer-tools/

NetBeans

Kurssilla käytetään oletuksena NetBeans-ohjelmointiympäristöä. Laitoksen koneille on asennettuna versio 7.2, mutta versiossa 7.3 tulee vielä parempi tuki HTML5:lle. Kurssin tehtävät palautetaan myös TMC:n kautta. Jos et halua käyttää NetBeansia, tehtävät voi palauttaa myös TMC:n webbisivuilta.

Huom! Toisin kuin aiemmalla web-palvelinohjelmointi -kurssilla, tällä kurssilla TMC ei tarkasta tehtävien oikeellisuutta. Palauttaessasi tehtävän lupaat sen olevan oikein.

Another easy start..

Huom! Tässä kurssiversiossa TMC ei tarkasta tehtävien oikeellisuutta. Palauttamalla tämän tehtävän vakuutat ja vannot että jatkossa palauttamasi tehtävät ovat parhaan ymmärryksesi mukaan ratkaistu oikein, ja ne toimivat kuten tehtävänannoissa on pyydetty.

Lue osoitteessa http://www.cs.helsinki.fi/group/java/s12-wepa/#w1e1 oleva tehtävä "NetBeansin ja TMCn asennus". Jos et ole jo asentanut NetBeansia ja TMC:tä, asenna ne, ja rekisteröidy TMC:hen. Kurssille rekisteröityminen tapahtuu kun palautat tämän tehtävän.

Huom! Valitse TMC:ssä kurssiksi s2012-weso.

Huom, aiemman toisto! Tässä kurssiversiossa TMC ei tarkasta tehtävien oikeellisuutta. Palauttamalla tämän tehtävän vakuutat ja vannot että jatkossa palauttamasi tehtävät ovat parhaan ymmärryksesi mukaan ratkaistu oikein, ja ne toimivat kuten tehtävänannoissa on pyydetty.

Internet

Web on täynnä selainohjelmointiin liittyviä artikkeleita. Oikeasti! Haku lauseella "html5 introduction" palauttaa hieman yli 17000 sivua. Jos avainsanat ovat erikseen, tuloksia on yli 10 miljoonaa. Kun teet kurssin tehtäviä, käytä googlea avuksi. Tätä materiaalia ei yritetäkään rakentaa kaiken kattavaksi, vaan joudut etsimään tietoa myös internetistä.

Jos mietit että miten vaikkapa article-elementille asetetaan reunat, voit googlettaa esimerkiksi avainsanoilla "html5 article css border". Ensimmäisen kymmenen artikkelin joukossa on (lähes) varmasti sinua auttava artikkeli. Itseasiassa, informaation hakeminen netistä on taito siinä missä ohjelmointikin -- sitä kannattaa ja pitää harjoitella.

HTML

HTML on kieli web-sivustojen luomiseen. HTML ei ole ohjelmointikieli, vaan kuvauskieli, jonka avulla kuvataan sekä web-sivun rakenne että sivun sisältämä teksti. HTML-sivujen rakenne määritellään HTML-kielessä määritellyillä elementeillä, ja yksittäinen HTML-dokumentti koostuu sisäkkäin ja peräkkäin olevista elementeistä.

Sivujen rakenteen määrittelevät elementit erotellaan normaalista tekstistä pienempi kuin (<) ja suurempi kuin (>) -merkeillä. Elementti avataan elementin nimen sisältävällä pienempi kuin -merkillä alkavalla ja suurempi kuin -merkkiin loppuvalla merkkijonolla, esim. <html>, ja suljetaan merkkijonolla jossa elementin pienempi kuin -merkin jälkeen on vinoviiva, esim </html>. Yksittäisen elementin sisälle voi laittaa muita elementtejä, ja elementti tulee sulkea aina lopuksi. Jos elementti ei sisällä muita elementtejä tai tekstiä, voi sen avata ja sulkea yksittäisellä komennolla, esim. <br/>.

HTML-dokumentin runko

Tyypillisen HTML-dokumentin runko näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
    </head>
    <body>

        <h1>Sivulla näkyvä otsikko</h1>

        <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>

    </body>
</html>

Yllä olevassa HTML-dokumentissa on dokumentin tyypin kertova erikoiselementti <!DOCTYPE html>, joka kertoo dokumentin olevan HTML-sivu. Tätä seuraa elementti <html>, joka aloittaa HTML-dokumentin. Elementti <html> sisältää yleensä kaksi elementtiä, elementit <head> ja <body>. Elementti <head> sisältää sivun otsaketiedot, eli esimerkiksi sivun käyttämän merkistön <meta charset="utf-8" /> ja otsikon <title>. Elementti <body> sisältää selaimessa näytettävän sivun rungon. Ylläolevalla sivulla on ensimmäisen tason otsake-elementti h1 (header 1) ja tekstielementti p (paragraph).

Elementit voivat sisältää attribuutteja ja attribuuttien arvoja. Yllä olevassa HTML-dokumentissa elementille meta on määritelty erillinen attribuutti charset, joka kertoo dokumentissa käytettävän merkistön: "utf-8". Attribuuttien lisäksi elementit voivat sisältää tekstisolmun. Esimerkiksi yllä olevat elementit title, h1 ja p kukin sisältävät oman tekstisolmun. Tekstisolmulle ei ole erillistä elementtiä tai määrettä, vaan se näkyy tekstinä.

Puhe tekstisolmuista antaa viitettä jonkinlaisesta puurakenteesta. HTML-dokumentit, aivan kuten XML-dokumentit, ovat rakenteellisia dokumentteja, joiden rakenne on usein helppo ymmärtää puumaisena kaaviona. Ylläolevan web-sivun voi esittää esimerkiksi seuraavanlaisena puuna (attribuutit ja dokumentin tyyppi on jätetty merkitsemättä).

                       html
                   /          \
                 /              \
              head              body
            /       \         /      \
         meta       title     h1      p
                     :        :       :
                  tekstiä  tekstiä tekstiä

Koska HTML-dokumentti on rakenteellinen dokumentti, on elementtien sulkemisjärjestyksellä väliä. Elementit tulee sulkea samassa järjestyksessä kuin ne on avattu. Esimerkiksi, järjestys <body><p>whoa, minttutee!</body></p> on väärä, kun taas järjestys <body><p>whoa, minttutee!</p></body> on oikea.

Web-sivujen läpikäynti

Kun selaimet lataavat HTML-dokumenttia, ne käyvät sen läpi ylhäältä alas, vasemmalta oikealle. Kun selain kohtaa elementin, se luo sille uuden solmun. Seuraavista elementeistä luodut solmut menevät aiemmin luodun solmun alle kunnes aiemmin kohdattu elementti suljetaan. Aina kun elementti suljetaan, puussa palataan ylöspäin edelliselle tasolle.

Miltä seuraavaa HTML-dokumenttia kuvaava puu näyttää?

<!DOCTYPE html>
<html>
    <head> 
    	<title>PSY: Gangnam Style</title>
    </head>
    <body>

        <p>"Gangnam Style" is a single by South Korean rapper PSY, that has been viewed 
        over 500 million times on <a href="http://www.youtube.com">YouTube</a></p>

    </body>
</html>

Entä seuraavalla HTML-dokumentilla?

<!DOCTYPE html>
<html>
    <head> 
    	<title>PSY: Gangnam Style</title>
    <body>

        <p>"Gangnam Style" is a single by South Korean rapper PSY, that has been viewed 
        over 500 million times on <a href="http://www.youtube.com">YouTube</p>

    </body>
</html>

Ascii Artist

Tehtäväpohjassa olevassa kansiossa src/main/webapp/ (tai NetBeansissa Web Pages) on dokumentti index.html. Muokkaa dokumenttia siten, että se sisältää seuraavannäköisen ASCII-taideteoksen (käytettävän fontin ei tarvitse olla sama).

Koska taideteos on ASCII-taidetta, et luonnollisestikaan saa käyttää sivussa kuvaa. Vinkki taideteoksen tekemiseen on yllä olevassa kuvassa. Kun taideteoksesi toimii Chromessa, palauta tehtävä TMC:lle.

Listaelementit

Sivuille voi lisätä listoja mm. ol (ordered list) ja ul (unordered list) -elementtien avulla. Elementeillä ol tai ul aloitetaan lista, ja listan sisälle asetettavat yksittäisiin listaelementteihin käytetään li (list item)-elementtiä. Yksittäiset listaelementit voivat taas sisältää esimerkiksi tekstisolmun tai lisää html-elementtejä.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
    </head>
    <body>

        <h1>Sivulla näkyvä otsikko</h1>

        <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä, 
        listaelementtiä voi käyttää esimerkiksi ostoslistan tekemiseen.</p>

        <ol>
            <li>kauraa</li>
            <li>puuroa</li>
            <li>omenaa</li>
        </ol>

    </body>
</html>

Yllä oleva lista näyttää seuraavalta ilman muita elementtejä.

  1. kauraa
  2. puuroa
  3. omenaa

Kuvien lisääminen

Jokaisen web-sivuja rakentavan ihmisen tulee ainakin kerran elämässään lisätä kuva web-sivuilleen. Sivuille saa lisättyä kuvia elementillä img, jolla on attribuutti src, jonka arvona on kuvan sijainti. Kuvan sijainti riippuu kuvan näyttävän html-tiedoston sijainnista. Jos kuva on samassa kansiossa html-dokumentin kanssa, tarvitsee img-elementin src-attribuutin arvoksi asettaa vain kuvan nimi.

Esimerkiksi, jos tämän html-tiedoston sisältämässä kansiossa on kansio nimeltä "img", ja siellä kuvatiedosto nimeltä "lamppu.png", saa kuvatiedoston sivuille näkyville elementillä <img src="img/lamppu.png" />. Koska kuvaelementti img ei sisällä muita elementtejä tai tekstiä, voi sen sulkea suoraan.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
    </head>
    <body>

        <h1>Sivulla näkyvä otsikko</h1>

        <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä, 
        listaelementtiä voi käyttää esimerkiksi ostoslistan tekemiseen.</p>

        <ol>
            <li>kauraa</li>
            <li>puuroa</li>
            <li>omenaa</li>
        </ol>

        <p>Kuvan saa taas näytettyä img-elementillä. Välähtikö?</p>

        <img src="img/lamppu.png" />
        
    </body>
</html>

Kuva ilman muita sivujen elementtejä näyttää seuraavalta.

Kuvien oikeuksista

Netissä olevat kuvat ja tiedostot eivät ole vapaasti kaikkien käytettävissä. Jos teet sivuja itsellesi, tutuille tai kavereille, ja käytät niissä netistä löytynyttä materiaalia, muista varmistaa että käyttämäsi kuvat ovat laillisesti käytettäviä. Kuvien käyttöoikeuksien varmistaminen ei ole aina helppoa tai edes mahdollista -- kannattaakin käyttää vain sivustoja, joiden oikeuksista on varmuus.

Esimerkiksi flickr-sivustolla on erillinen creative commons-osio, joka listaa kuvia, joiden käyttö on sallittua tietyin ehdoin. Löydät eri ehdot ja kuvia osoitteesta http://www.flickr.com/creativecommons/. On myös sivuja, jotka tarkoituksella keräävät materiaalia tiettyihin aiheisiin liittyen. Esimerkiksi sivusto OpenGameArt tarjoaa vapaasti peleissä käytettäviä materiaaleja.

Linkit toisille sivuille

Elementin a (anchor) avulla voi luoda linkin sivulta toiselle. Sivu, jolle käyttäjä siirtyy, merkitään elementin a attribuutin href arvolla. Jos sovelluksessasi on kaksi sivua, index.html ja oma.html, voi sivulta oma.html luoda linkin sivulle index.html komennolla <a href="index.html">index.html</a>. Sivulta voi lisätä myös linkin täysin toiselle sivulle. Alla olevassa esimerkissä on linkki YouTube-sivustolle.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
    </head>
    <body>

        <h1>Sivulla näkyvä otsikko</h1>

        <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä, 
        listaelementtiä voi käyttää esimerkiksi ostoslistan tekemiseen.</p>

        <ol>
            <li>kauraa</li>
            <li>puuroa</li>
            <li>omenaa</li>
        </ol>

        <p>Kuvan saa taas näytettyä img-elementillä. Välähtikö?</p>

        <img src="img/lamppu.png" />

        <p>Linkkejä saa lisättyä a-elementillä: <a href="http://www.youtube.com/watch?v=9bZkp7q19f0">klikkaamalla 
        liityt miljoonien joukkoon.</a></p>
        
    </body>
</html>

Yllä olevan sivun viimeinen tekstielementti näyttää seuraavalta:

Linkkejä saa lisättyä a-elementillä: klikkaamalla liityt miljoonien joukkoon.

Linkki-elementeille voi lisätä myös attribuutin target, jolla voi ilmaista tietyn ikkunan, johon sivu avataan. Jos attribuutille target antaa arvon "_blank", avataan linkki aina uuteen ikkunaan.

HTML5 ja apuvälineet sivun rakenteen määrittelyyn

HTML5, eli HTML-kuvauskielen uusin (vielä kesken oleva) versio, toi mukanaan sivun rakenteen suunnittelua helpottavia elementtejä. Sivun rakenteen määrittelyä helpottavat elementit header, jonka sisälle kirjoitetaan sivun yleinen alkuosa kuten h1-elementti ja valikko, nav, joka sisältää sivun valikon, section-elementti, joka esimerkiksi nivoo yhteen toisiinsa liittyviä osia, article, joka sisältää yksittäisen sivulla olevan dokumentin, ja footer, joka kertoo sivun loppuosan. Näiden avulla sivun saa jaettua loogisiin osakokonaisuuksiin.

Rakennetta helpottavien elementtien käyttö ja toiminta liittyy elementtiin, jonka sisällä ne ovat. Jos elementtiä header käytetään elementin article sisällä, on header luonnollisesti artikkelin otsaketiedot. Jos taas header-elementti on body-elementin sisällä, liittyy header-elementin sisältö itse sivuun.

Sivut koostuvat yleensä header-elementillä merkittävästä yläosasta, jossa on otsikko ja mahdollisesti nav-elementillä merkitty valikko. Näitä seuraa yksi tai useampi tekstiosa, joka merkitään article-elementillä. Sivun lopussa on elementti footer, joka sisältää esimerkiksi yhteystiedot.

Seuraavassa on esimerkki, jossa h1-otsikko on asetettu header-elementin sisään. Sivulla on kaksi erillistä kirjoitusosaa, jotka on eroteltu article-elementeillä. Näitä seuraa lopulta footer-elementillä merkitty alaosa.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>

        <header>
            <h1>Sivulla näkyvä otsikko</h1>
        </header>
        
        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
        </article>

        <article>
            <ol>
                <li>kauraa</li>
                <li>puuroa</li>
                <li>omenaa</li>
            </ol>
        </article>

        <footer>
            <p>alatunniste, esimerkiksi tekijöiden nimet.</p>
        </footer>
    </body>
</html>

Sivulla näkyvä otsikko

Sivuilla näytettävä normaali teksti on p-elementin sisällä.

  1. kauraa
  2. puuroa
  3. omenaa

Rakenteellinen lähestymistapa sivujen sisällön määrittelyyn

HTML-kuvauskieltä käytetään sivujen rakenteen määrittelyyn. Ennen HTML5:ttä sivun elementtejä eroteltiin toisistaan div (divider)-elementeillä, joille määriteltiin attribuuttina sivun osa, jonka div-elementti sisälsi. Yllä olevan sivun rakenne voidaan luoda myös div-elementeillä.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>

        <div class="header">
            <h1>Sivulla näkyvä otsikko</h1>
        </div>
        
        <div class="article">
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
        </div>

        <div class="article">
            <ol>
                <li>kauraa</li>
                <li>puuroa</li>
                <li>omenaa</li>
            </ol>
        </div>

        <div class="footer">
            <p>alatunniste, esimerkiksi tekijöiden nimet.</p>
        </div>
    </body>
</html>

Huomannet että ero on käytännössä hyvin pieni. Oleellisinta on loogisten osakokonaisuuksien erottelu toisistaan. HTML5 tarjoaa siihen työvälineet, joita kannattaa käyttää.

Kampuskuoro

Luo tehtäväpohjassa olevaan kansioon src/main/webapp/ uusi sivu index.html. Muokkaa sivua siten, että se näyttää seuraavalta selaimessa:

Otsikon tulee olla header-elementin sisällä, kuvaus ja laululista omien article-elementtien sisällä. Ei haittaa jos tekstin leveys on eri kuin yllä olevassa kuvassa! Kun olet valmis, ja sivusi näyttää oikealta Chromessa, palauta tehtävä TMC:lle.

Lomakkeet

Lomakkeita käytetään tiedon syöttämiseen web-sivuille. Tietoa voi lähettää joko erilliselle palvelimelle, tai käsitellä osana sivustoa JavaScript-kielen avulla. Lomakkeet aloitetaan HTML-elementillä <form>. Lomake-elementin sisälle voi asettaa useita erilaisia kenttiä. Palvelimelle dataa lähetettäessä jokaisella kentällä tulee olla attribuutti name, jota käytetään palvelimella tiedon identifiointiin.

Erilaisia lomakekenttiä on useita:

Lomakkeen lähettäminen

Kun lomake lähetetään selain ohjaa käyttäjän kohdeosoitteeseen siten, että lähetettävän lomakkeen tiedot ovat mukana selaimen tekemässä pyynnössä. Jos lomakkeen lähetystapa on GET, on lomakkeen tiedot osana osoitetta. Lähetystavassa POST arvot tulevat osana pyynnön runkoa.

Alla on lomake jolla voi visualisoida tietojen lähettämistä palvelimelle. Lomakkeet lähetetään osoitteeseen http://t-avihavai.users.cs.helsinki.fi/lets/See), jossa on pyynnössä saadut tiedot tulostava web-palvelu.

<form method="POST" action="http://t-avihavai.users.cs.helsinki.fi/lets/See">
    <label>Käyttäjätunnus: <input type="text" name="tunnus" /></label>
    <label>Salasana: <input type="password" name="salasana" /></label>
    <input type="submit" />
</form>

Lisää informaatiota

Mikään yksittäinen HTML-opas ei itsessään kata kaikkea HTMLään liittyvää. Tässäkin dokumentissa tehtiin pieni pintaraapaisu HTMLn ominaisuuksiin. Erityisesti selainsovelluksia rakennettaessa uusinta informaatiota jaetaan mm. blogeissa ja muissa webissä julkaistavissa artikkeleissa. Web on täynnä timanttisia sivustoja kuten http://www.html5rocks.com/, http://html5-demos.appspot.com/, ...

Kuten yllä olevista linkeistä huomaat, "puhekielessä" HTML5 sisältää HTML-syntaksin lisäksi CSSn ja JavaScriptin. Avain onneen on pienestä liikkeelle lähteminen: toteutta joku webissä oleva hieno sivu itse sivun lähdekoodia seuraten. Muokkaile lähdekoodia, ja ota selvää mitä eri komennot tekevät. Kun joku komento on epäselvä, google auttaa.

CSS

CSS (cascading style sheets)-tyylitiedostot ovat tiedostoja, joissa määritellään miten web-sivun elementit tulee näyttää käyttäjälle. HTML-kuvauskielellä määritellään web-sivun rakenne ja sisältö, tyylitiedostoilla sen ulkoasu.

Tyylitiedostoilla voi tehdä ison eron siihen, miltä sivu näyttää. Lähdetään kuitenkin pienestä liikkeelle. Tyylitiedosto on HTML-dokumentista erillinen tiedosto, joka sisältää erilaisia tyylimäärittelyjä. Tyylitiedostoja voi olla useita. Jotta HTML-dokumentti saa tyylitiedoston käyttöönsä, tulee tyylitiedoston sijainti määritellä head-elementin sisälle tyylitiedoston lataavaan elementtiin link.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
        <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>

        <!-- sivun sisältö: näin sivuille saa kommentin -->

    </body>
</html>

Elementille link kerrotaan viitattavan tiedoston tyyli (rel="stylesheet"), tyyppi (type="text/css") ja sijainti (href="sijainti.css"). Sijainnin tulee kertoa tyylitiedoston nimi. Tyylitiedostojen päätteenä käytetään merkkijonoa .css. Esimerkiksi jos tyylitiedosto tämän tiedoston sisältämän kansion sisällä olevassa kansiossa "stylesheets" ja tyylitiedoston nimi on style.css, asetetaan elementin link attribuutin href arvoksi "stylesheets/style.css".

Tyylitiedosto

Tyylitiedosto on erillinen tiedosto HTML-dokumentista. Luodaan esimerkiksi tiedosto style.css, jonka sisältö on seuraavanlainen:

body {
    background-color: rgb(200, 200, 200);
    margin: 0;
    padding: 0;
}

Yllä olevassa tyylitiedostossa sanotaan, että elementin body (eli HTML-dokumentin rungon) taustaväri on rgb-arvolla kerrottuna 200, 200, 200, eli vaaleahko. Väriarvo rgb tulee sanoista red, green, ja blue, ja jokaisella arvolla kerrotaan värin määrän. Kunkin värin määrä ilmaistaan numerolla nollan ja 255 välillä. Jos jokaisen värin arvo on 0, on väri musta, ja jos jokaisen värin arvo on 255, on väri valkoinen.

Kukin tyylimäärittely alkaa tyyliteltävän elementin kertovalla valitsimella ja avaavalla aaltosululla {, joita seuraa listaus käytettävistä tyyleistä. Kun käytettävät tyylit on määritelty, tyylimäärittely lopetetaan sulkevalla aaltosululla }. Kullakin tyylillä on nimi ja arvo, jotka erotetaan toisistaan kaksoispisteellä :. Yksittäisen tyylin (nimi ja arvo) jälkeen lisätään puolipiste ;. Yhteen tyylimäärittelyyn voi sisällyttää useita tyylejä, ja yksittäinen tyyli voi riippuen tyylistä saada useita arvoja.

valitsin {
    tyylin-nimi: tyylin-arvo;
    toisen-tyylin-nimi: arvo toinen-arvo;
}

Tyylimäärittely voi myös sisältää useita tyyliteltäviä elementtejä, tällöin valitsimet erotellaan toisistaan pilkulla.

valitsin, valitsin2 {
    tyylin-nimi: tyylin-arvo;
    toisen-tyylin-nimi: arvo toinen-arvo;
}

Valitsimia voi käyttää myös lasten valintaa, esimerkiksi seuraavassa valitaan ensimmäisen valitsimen joukosta toisen valitsimen tyyppinen lapsi.

valitsin > valitsin2 {
    tyylin-nimi: tyylin-arvo;
    toisen-tyylin-nimi: arvo toinen-arvo;
}

Valitsimet

Jokaiselle sivun elementille voidaan määritellä oma tyyli. Jos halutaan että sama tyyli esiintyy kaikissa elementeissä, voidaan valitsimena käyttää elementin nimeä. Esimerkiksi seuraava tyylitiedosto muuttaa kaikkien p-elementtien tekstin värin punaiseksi.

p {
    color: rgb(255, 0, 0);
}

Luokka-attribuutti

Silloin tällöin tyyli halutaan asettaa vain tietylle elementille tai elementtijoukolle. Elementeille voidaan määritellä luokka-attribuutti class, jonka arvoksi asetetaan joku tietty arvo, esimerkiksi "blue". Luokka-attribuuttien tyylit voi asettaa erillisellä class-attribuutteja valitsevalla valitsimella, pisteellä. Esimerkiksi kaikki elementit, joilla on luokka-attribuutin arvo "blue" voi valita seuraavasti (kaikille asetetaan alla tekstin väriksi sininen):

.blue {
    color: rgb(0, 0, 255);
}

Itse sivulla olevat elementit näyttävät seuraavalta luokka-attribuutin kanssa. Osalla elementeistä on luokka-attribuutti "blue", ja osalla ei.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
        <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>

        <header>
            <h1>Sivulla näkyvä otsikko</h1>
        </header>
        

        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
            <p class="blue">Kuten edellinen elementti, mutta tällä on luokka-attribuutti, jonka arvo on "blue".
            Tyylimäärittely .blue asettaa tekstin värin siniseksi.</p>
        </article>
        
    </body>
</html>

Sivulla näkyvä otsikko

Sivuilla näytettävä normaali teksti on p-elementin sisällä.

Kuten edellinen elementti, mutta tällä on luokka-attribuutti, jonka arvo on "blue".

Tunnus-attribuutti

Luokka-attribuuttia käytetään joukolle tyylejä. Yksittäisiä elementtejä tyyliteltäessä tapana on käyttää erillistä tunnus-attribuuttia, joka määritellään nimellä id. Kuten luokka-attribuutille, myös tunnus-attribuutille asetetaan arvo. Tunnus-attribuuttiin pääsee käsiksi tyylitiedostossa risuaita (#) -etuliitteellä. Luodaan sivu, jossa on useampia artikkeleita, joista yhtä halutaan korostaa.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
        <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>
        <header>
            <h1>Sivulla näkyvä otsikko</h1>
        </header>
        

        <article id="highlighted-article">
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
        </article>

        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
        </article>

        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä.</p>
        </article>
                
    </body>
</html>

Luodaan sivu siten, että artikkeleilla on pyöreät kulmat ja vaaleahko taustaväri. Korostettavalla artikkelilla on oma tyylinsä, jossa sille asetetaan hieman vaaleampi taustaväri.

article {
    background-color: rgb(200, 200, 200);
    margin: 1em;
    padding: 1em;
    width: 40%;

    /* pyöreät kulmat -- useampi määrittely sillä tyylispesifikaatio vielä kesken */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

#highlighted-article {
    width: 42%;
    background-color: rgb(240, 240, 240);

    /* fiilistellään css3-tyylien kanssa */
    -webkit-transform: rotate(-2deg); /* chrome, safari */  
    -moz-transform: rotate(-2deg); /* firefox */  
    -o-transform: rotate(-2deg); /* opera */  
    transform: rotate(-2deg);  
}

Sivulla näkyvä otsikko

Sivuilla näytettävä normaali teksti on p-elementin sisällä.

Sivuilla näytettävä normaali teksti on p-elementin sisällä.

Sivuilla näytettävä normaali teksti on p-elementin sisällä.

Tyylien vaikutus elementteihin

Edellä olevassa esimerkissä tyyli #highlighted-article ei sisältänyt reunojen pyöristystä, mutta silti korostetun artikkelin reunat pyöristettiin silti. Miksi?

Elementti käyttää kaikkia sille määriteltyjä tyylejä. Esimerkiksi tyyli #highlighted-article on osana article-elementtiä, jolla taas on siihen liittyvä tyyli. Mielenkiintoinen kohta liittyy artikkelin leveyteen (tyylimäärittely width): tyylissä #highlighted-article oleva määrittely width korvaa tyylissä article määritellyn leveyden. Jos leveyttä ei olisi korvattu, olisi myös leveys peritty.

Koska HTML-dokumentti on puu, voi tyylien periytymistä ajatella myös puumaisena periytymisenä. Jos elementille body määritellään tietynlainen tyyli, on kaikilla sen lapsisolmuilla body-elementissä määritelty tyyli, ellei lapsisolmu korvaa tyyliä.

Kaikki tyylit eivät kuitenkaan periydy. Tutustu tarkemmin tyylimäärittelyihin ja standardiin osoitteessa http://www.w3.org/TR/CSS/#properties.

Css Askeleet

Luo tehtäväpohjan kansioon src/main/webapp/ tyylitiedosto style.css, ja viittaa siihen tiedostosta index.html. Muokkaa tyylitiedostoa style.css siten, että sivu index.html näyttää seuraavanlaiselta.

Tässä muutama hyödyllinen väri:

Sivu käyttää seuraavaa määrittelyä fontin valintaan..

    font-family: 'Trebuchet MS', Trebuchet, Arial, sans-serif;

Kun olet valmis, ja sivusi näyttää oikealta Chromessa, palauta tehtävä TMC:lle.

Case: Listojen tyylit

Tyylitiedostoilla saa muokattua oikeastaan kaikkea sivulla olevaa. Listalle voi asettaa taustavärin, ja siitä voi poistaa numeroinnin tai pallot. Luodaan tyyliluokka menu, jossa listan taustaväri on vaalean harmaa, ja listan palloja ei näytetä.

.menu {
    /* listan tausta on harmaa */
    background-color: rgb(230, 230, 230);

    /* ei näytetä palloja */
    list-style-type: none;
    
    /* lisätään reunoille hieman tilaa (1em = 1 standardimerkin leveys) */
    padding: 1em;
}
  <ul class="menu">
    <li>Eka pallukka</li>
    <li>Toka pallukka</li>
    <li>Kolmas pallukka</li>
  </ul>

Nyt lista näyttää seuraavalta:

Muutetaan listan elementtejä siten, että ne asetellaan vierekkäin. Luodaan tyyliluokka menuelement, joka asettaa listaelementit vierekkäin, ja lisää niille hieman tilaa sivuille.

.menuelement {
    /* 1 standardimerkin leveyden verran tilaa jokaiselle puolelle */
    padding: 1em;
    /* näytetään menuelementit vierekkäin */
    display: inline; 
}

Lisätään listan elementeille tyyliluokka listaelementti.

  <ul class="menu">
    <li class="menuelement">Eka pallukka</li>
    <li class="menuelement">Toka pallukka</li>
    <li class="menuelement">Kolmas pallukka</li>
  </ul>

Nyt lista näyttää seuraavalta:

Valikkoihin halutaan usein jonkinlaista dynaamista toiminnallisuutta. Lisätään toiminnallisuus, jossa vaihtoehdon taustaväri muuttuu kun sen päälle viedään hiiri. Valitsimen lisämääreellä :hover voidaan määritellä tyyli, joka näkyy vain kun hiiri on tyylitellyn alueen päällä. Lisätään toinen menuelement-tyyliluokka, ja sille lisämääre :hover.

.menuelement {
    /* 1 standardimerkin leveyden verran tilaa jokaiselle puolelle */
    padding: 1em;
    /* näytetään menuelementit vierekkäin */
    display: inline; 
}

.menuelement:hover {
    /* vaaleampi taustaväri kun hiiri on tyylin päällä */
    background-color: rgb(245, 245, 245);
}

Tässä meidän ei tarvitse muokata HTML-dokumenttia, sillä tyyliluokka menuelement on jo määritelty HTML-dokumenttiin.

Vielä noin 5 vuotta sitten pyöreät kulmat tehtiin erillisillä kuvilla. Ei enää! Pyöreät kulmat eivät ole vielä ihan helpon komennon takana, vaan niihin tarvitaan kolme erillistä komentoa. Erillisiä komentoja tarvitaan selainyhteensopivuuden varmistamiseksi: pyöreät kulmat määrittelevä standardi ei ole vielä lopullinen...). Pyöreät kulmat saa lisättyä tyyliluokkaan menu seuraavasti:

.menu {
    /* tämä on muuten kommentti, eli kone ei tee sillä mitään */
    /* listan tausta on harmaa */
    background-color: rgb(230, 230, 230);

    /* ei näytetä palloja */
    list-style-type: none;

    /* lisätään reunoille hieman tilaa (1em = 1 standardimerkin leveys) */
    padding: 1em;

    /* pyöreät kulmat */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

Lista näyttää nyt seuraavalta:

Tyylivalitsimet ja menu

Yllä oleva lähestymistapa, vaikkakin hieno, on hieman kömpelö. Jouduimme lisäämään jokaiselle tyyliteltävälle elementille oman luokkamäärittelyn. Aiemmin näimme, että tyylejä voi määritellä listana seuraavasti:

valitsin, valitsin2 {
    tyylin-nimi: tyylin-arvo;
    toisen-tyylin-nimi: arvo toinen-arvo;
}

Tutustutaan seuraavaksi toiseen tapaan. Tavoitteenamme on tyylitellä alla olevan sivun header-osio uudestaan. Huomaa jo nyt, että sivun header-osioon ei ole määritelty luokkia tai tunnuksia!

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
        <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>
        <header>
            <h1>Sivulla näkyvä otsikko</h1>

            <nav>
                <ul>
                    <li><a href="#">linkki</a></li>
                    <li><a href="#">linkki</a></li>
                    <li><a href="#">linkki</a></li>
                    <li><a href="#">linkki</a></li>
                </ul>
            </nav>

        </header>

        <!-- muu sisältö -->
                
    </body>
</html>

Sivulla näkyvä otsikko

Toinen tapa valita tyyliteltäviä elementtejä liittyy niiden järjestykseen osana dokumenttia. Voimme tyylitellä header-elementin sisällä olevan h1-elementin seuraavasti. Huomaa että tämä tyylittely ei muuta kaikkia h1-elementtejä, vain vaan ne, jotka ovat header-elementin sisällä.

header h1 {
    color: rgb(80, 80, 80);
}

Sivulla näkyvä otsikko

Jatketaan esimerkkiä muokkaamalla header-elementissä olevan nav-elementin sisältämää listaa. Listan taustaväriksi asetetaan lähes musta, ja sillä on pyöristetyt kulmat.

header h1 {
    color: rgb(40, 40, 40);
}

header nav ul {
    /* fontin koon ym määrittelyä.. */
    font-size: 1.2em;
    height: 40px;
    line-height: 30px;
    margin: 0 auto 2em auto;

    /* taustaväriksi h1-elementin taustaväri */
    background-color: rgb(40, 40, 40);

    /* ei näytetä palloja */
    list-style-type: none;


    /* pyöreät kulmat */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

Sivulla näkyvä otsikko

Ei mitenkään kovin komea, muutamat selaimet näyttävät myös listaelementit listan ulkopuolella. Lisätään listaelementeille määrittely, jossa ne asetetaan vierekkäin.

header h1 {
    color: rgb(40, 40, 40);
}

header nav ul {
    /* fontin koon ym määrittelyä.. */
    font-size: 1.2em;
    height: 40px;
    line-height: 30px;
    margin: 0 auto 2em auto;

    /* taustaväriksi h1-elementin taustaväri */
    background-color: rgb(40, 40, 40);

    /* ei näytetä palloja */
    list-style-type: none;


    /* pyöreät kulmat */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

header nav ul li {
    float: left; 
    display: inline; 
}

Sivulla näkyvä otsikko

Ei vieläkään komea, mutta selviämme tästä kyllä. Muutetaan linkkielementtien väri valkoiseksi, suurennetaan niiden fonttia, ja asetetaan niille hieman tilaa ympärille.

header h1 {
    color: rgb(40, 40, 40);
}

header nav ul {
    /* fontin koon ym määrittelyä.. */
    font-size: 1.2em;
    height: 40px;
    line-height: 30px;
    margin: 0 auto 2em auto;

    /* taustaväriksi h1-elementin taustaväri */
    background-color: rgb(40, 40, 40);

    /* ei näytetä palloja */
    list-style-type: none;


    /* pyöreät kulmat */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

header nav ul li {
    float: left; 
    display: inline; 
}


header nav ul li a {
    color: rgb(255, 255, 255);
    display: inline-block;
    padding: 5px 1.5em;
    text-decoration: none;
}

Nyt sivumme yläosa näyttää seuraavalta.

Sivulla näkyvä otsikko

Lisätään vielä linkkielementeille toiminnallisuus, jossa niiden taustaväri muuttuu kun hiiri viedään elementin päälle. Asetetaan taustaväri tällöin valkoiseksi, ja linkin fontti aiemmin käytetyksi tummaksi väriksi.

header h1 {
    color: rgb(40, 40, 40);
}

header nav ul {
    /* fontin koon ym määrittelyä.. */
    font-size: 1.2em;
    height: 40px;
    line-height: 30px;
    margin: 0 auto 2em auto;

    /* taustaväriksi h1-elementin taustaväri */
    background-color: rgb(40, 40, 40);

    /* ei näytetä palloja */
    list-style-type: none;

    /* pyöreät kulmat */
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
}

header nav ul li {
    float: left; 
    display: inline; 
}

header nav ul li a {
    color: rgb(255, 255, 255);
    display: inline-block;
    padding: 5px 1.5em;
    text-decoration: none;
}

header nav ul li a:hover {
    background-color: rgb(255, 255, 255);
    color: rgb(40, 40, 40);
}

Sivulla näkyvä otsikko

Done! Tyylimaailman sopivuudesta jokainen saa toki päättää itse :).

Suosikit

Tehtäväpohjassa on lista suosikkeja. Tehtävänäsi on lisätä sivulle tyyli, joka tekee sivusta seuraavannäköisen. Huomaa, että sivun artikkeli-elementtien tulee kääntyä hieman kun hiiri viedään niiden päälle.

Edellisessä tehtävässä määrittelemistäsi tyyleistä on hyötyä tässä tehtävässä. Kun olet valmis, ja sivusi näyttää oikealta Chromessa, palauta tehtävä TMC:lle.

Lisää informaatiota

Web on pullollaan myös CSSään liittyviä sivuja; http://www.css3.com/, http://www.smashingmagazine.com/learning-css3-useful-reference-guide/, ...

Viimeisin versio löytyy osoitteesta http://www.w3.org/Style/CSS/current-work -- CSS3 on jatkuvasti päivittyvä standardi.

JavaScript

Siinä missä HTML on kuvauskieli web-sivujen rakenteen ja sisällön luomiseen, ja CSS on kieli web-sivustojen tyylin määrittelyyn, JavaScript on kieli dynaamisen toiminnan lisäämiselle. Suomen kielioppia ajatellen HTML:ää voi ajatella substantiiveina, CSS:ää adjektiiveina, ja JavaScriptiä verbeinä. Käytännössä JavaScript on ohjelmakoodia, jota suoritetaan tarvittaessa komento kerrallaan -- ylhäältä alas, vasemmalta oikealle.

Ensimmäinen komento jonka opimme on alert("tulostettava merkkijono"). Funktio alert("jotain") on JavaScriptin valmis funktio, ja se avaa uuden pop-up -ikkunan jossa näkyy funktion alert parametrina saama arvo. Funktiolle alert voi antaa parametrina oikeastaan minkälaisia arvoja tahansa. Voit testata alert-funktion toimintaa alla olevassa laatikossa. Painamalla nappia "Suorita koodi!", selaimesi suorittaa laatikossa olevan koodin.

Ohjelmakoodia suoritettaessa selain käy läpi laatikossa olevat komennot yksi kerrallaan ylhäältä alas, vasemmalta oikealle, ja toimii niiden mukaan. JavaScript-koodi suoritetaan siis omalla koneellasi omassa selaimessasi. Kukin komento loppuu puolipisteeseen (;), ja komentoja voi olla useampia. Mitä käy jos muutat alert-komennossa olevaa tekstiä, tai lisäät useamman alert-komennon?

Vaikkakin käytämme komentoa alert hyvin intensiivisesti, se ei ole osa JavaScript-spesifikaatiota.

 

 

Funktiot

Jos ylläoleva koodi asetetaan lähdekooditiedostoon, se suoritetaan heti kun selain lataa tiedoston. Haluamme kuitenkin usein siirtää koodin suoritusta tulevaisuuteen, johonkin tiettyyn tapahtumaan. Tätä varten on olemassa funktiot. Funktioilla määritellään ohjelmakoodia, joka suoritetaan myöhemmin.

Funktio määritellään merkkijonolla function funktionNimi() { suoritettava koodi}. Ensin avainsana function, joka kertoo että seuraavaksi on tulossa funktion määrittely. Tätä seuraa funktion nimi, ja sulut, joita seuraa aukeava aaltosulku {. Aaltosulun jälkeen tulee funktiota kutsuttaessa suoritettava ohjelmakoodi, jonka jälkeen tulee sulkeva aaltosulku }.

Alla on määritelty funktio, joka kysyy käyttäjältä nimeä, tallentaa nimen muuttujaan nimi, ja lopulta käyttää funktiota alert nimen tulostamiseen.

Kun painat nappia "Suorita koodi!", huomaat ettei mitään tapahdu. Tämä johtuu siitä, että funktiota ei kutsuta mistään, eli funktion suorittamista ei pyydetä. Lisätään funktiokutsu. Funktion kutsuminen onnistuu sanomalla funktion nimi ja sulut, sekä puolipiste. Esimerkiksi funktionNimi();. Alla olevassa koodissa on sekä funktion kysyNimiJaTervehdi määrittely, että funktion kysyNimiJaTervehdi kutsu.

JavaScriptin lisääminen omille sivuille

Aivan kuten CSS-tyylimäärittelyt, JavaScript-lähdekoodit tulee erottaa HTML-dokumentista.

JavaScript-tiedoston pääte on yleensä .js ja siihen viitataan elementillä script. Elementillä script on attribuutti src, jolla kerrotaan lähdekooditiedoston sijainti. Jos lähdekoodi on kansiossa javascript olevassa tiedostossa code.js, käytetään script-elementtiä seuraavasti: <script src="javascript/code.js"></script>. Huomaa että script-elementti suljetaan poikkeuksellisesti erikseen vaikka se ei sisälläkään tekstiä.

Hyvä käytäntö JavaScript-lähdekoodien lataamiseen on ladata ne juuri ennen niiden tarvitsemista. Jos lähdekoodia ei tarvita kuin vasta sivun ollessa kokonaan latautunut, kannattaa lähdekoodien hakeminen asettaa sivun loppuun. Tämä johtuu mm. siitä, että selaimen kohdatessa JavaScript-tiedoston, se lähtee hakemaan tiedostoa ja asettaa kaikki muut toiminnot odottamaan. Kun lähdekooditiedosto ladataan vasta sivun lopussa, käyttäjä näkee sivun sisältöä jo ennen lähdekoodin latautumista: tämä luo tunteen nopeammin reagoivista sivuista.

Luodaan kansioon javascript lähdekooditiedosto code.js. Tiedostossa code.js on funktio sayHello, joka näyttää käyttäjälle viestin "BAD = browser application development".

function sayHello() {
    alert("BAD = browser application development");
}

HTML-dokumentti, jossa lähdekooditiedosto ladataan, näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
        <link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
    	<title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>
        <header>
            <h1>Sivulla näkyvä otsikko</h1>
        </header>
        
        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä. Alla on nappi, 
            jota painamalla kutsutaan funktiota "sayHello".</p>
            <input type="button" value="Tervehdi" onclick="sayHello();" />
        </article>

        <!-- ladataan JavaScript-koodit tiedoston lopussa! -->
        <script src="javascript/code.js"></script>
                
    </body>
</html>

Itse sivu näyttää seuraavalta:

Sivulla näkyvä otsikko

Sivuilla näytettävä normaali teksti on p-elementin sisällä. Alla on nappi, jota painamalla kutsutaan funktiota "sayHello".

Mitä eläin sanoo?

Tehtäväpohjassa tulevalla sivulla on kolme erillistä eläintä. Luo tehtäväpohjaan erillinen javascript-tiedosto, ja lataa se sivun käyttöön. Lisää toiminnallisuus, jonka avulla lehmän nappia painettaessa käyttäjälle tulostetaan viesti "muu muu", porsaan nappia painettaessa "nöf nöf", ja kanan nappia painettaessa "kot kot".

Varmista sivusi toiminnallisuus. Kun olet valmis, palauta tehtävä TMC:lle.

JavaScriptin alkeita

Tässä osiossa käydään pikaisesti läpi JavaScriptin alkeita.

Muuttuja

Jotta sama tieto olisi käytössä useammassa paikassa, tarvitsemme jonkun tavan tallentaa tietoa. Javascriptissä, kuten lähes kaikissa muissakin ohjelmointikieliessä, tähän käytetään muuttujia. Muuttujat esitellään sanomalla var muuttujanNimi, eli ensin sana var, jota seuraa nimi muuttujalle. Tämän jälkeen seuraa yhtäsuuruusmerkki, jota seuraa muuttujaan asetettava arvo, esimerkiksi var vitonen = 5;. Edellinen komento luo muuttujan vitonen, ja asettaa siihen arvon 5.

Alla olevassa koodissa asetamme ensin muuttujaan kolme arvon 3, ja kutsumme aiemmin näkemäämme alert-komentoa siten, että alert-komento saa parametrina muuttujassa kolme olevan arvon.

Olemassaoleviin muuttujiin voi sijoittaa uuden arvon. Seuraava koodi näyttää ensin numeron 3, ja sitten numeron 4. Huomaa että sana var esiintyy vain kun muuttuja esitellään ensimmäisen kerran. Tämän jälkeen muuttuja on jo olemassa ja sanaa var ei enää tarvita.

Muuttujia voi myös laskea yhteen. Koulussa opittu pluslasku toimii kuten tähänkin mennessä.

Yllä luodaan ensin muuttuja kolme, ja asetetaan siihen arvo 3. Tämän jälkeen luodaan uusi muuttuja nimeltä nelja, ja asetetaan siihen arvo 4. Tämän jälkeen luodaan uusi muuttuja seitseman, ja asetetaan siihen aiemmin määriteltyjen muuttujien (kolme ja nelja) arvojen summa.

Muuttujien tyypit

JavaScript, toisin kuin peruskursseillamme käytetty Java, ei ole vahvasti tyypitetty ohjelmointikieli. Tämä tarkoittaa sitä, että ohjelmointikieli ei rajoita muuttujissa käytettävien arvojen tyyppiä. Muuttujan tyyppi voi olla numero, merkkijono tai vaikkapa funktio (palaamme tähän myöhemmin...). Muuttujaan voi siis asettaa myös merkkijonon. Merkkijono aloitetaan ja lopetetaan hipsuilla ("").

Merkkijonojen katenaatio eli yhdistäminen onnistuu summamerkillä Javasta tutulla tavalla.

Yllä muuttujaan summa asetetaan muuttujien eka ja toka liitos, eli merkkijono "23".

Jos merkkijonot haluaa muuttaa luvuiksi, tulee muunnoksessa käyttää JavaScriptin parseInt-funktiota. Komento parseInt muuttaa parametrina saadun merkkijonon kokonaisluvuksi.

Arvojen vertaileminen ja lisää tyypeistä

Ohjelmiin tuodaan vaihtoehtoista toiminnallisuutta muuttujien ja vertailuoperaatioiden yhteistyöllä. Luodaan ohjelma, joka kysyy käyttäjältä numeroa. Jos käyttäjän antama numero on 5, sanotaan "Oikein meni!". Vertailu onnistuu if-lauseella ja kahdella yhtäsuuruusmerkillä. Koska funktio prompt palauttaa merkkijonon, muunnetaan saatu luku numeroksi JavaScriptin parseInt-funktiolla.

Jos haluamme sanoa "Oikein meni!" kun käyttäjä antaa numeron 5 tai 7, voimme tehdä erillisen else if-vertailun. Vertailu else if tulee aina vertailun if jälkeen, ja vertailua else if ei voi käyttää ilman if-vertailua.

Lauseita else if voi olla peräkkäin rajoittamaton määrä. Ohjelmakoodissa voi käyttää myös Javasta tuttuja || (tai) ja && (ja) -operaatioita vertailutulosten yhdistämiseksi.

Joskus haluamme tulostaa jotain, vaikka yksikään vertailu ei onnistuisi. Tällöin käytämme ehtoa else, joka tarkoittaa "muutoin". Lisätään yllä olevaan luvun tarkistamiseen else-lause, joka suoritetaan kun yksikään aiemmista ehdoista ei onnistunut.

Muutkin Javasta tutut vertailuoperaatiot ovat käytössä. Suurempi kuin > merkillä tarkistetaan onko luku suurempi kuin joku luku, ja pienempi kuin < merkillä tarkistetaan onko luku pienempi kuin joku luku. Tehdään ohjelma, joka kysyy ikää, ja sanoo "Huijaat!", jos ikä on pienempi kuin 0 tai suurempi kuin 120. Muulloin ohjelma sanoo "Et huijannut! :)" .

Yllä olemme vertailleet juurikin muuttujien arvoja. Vertailuoperaattorin == yksi päänvaivaa tuottava ominaisuus on se, että vertailuoperaatio ei välitä muuttujan tyypistä. Esimerkiksi seuraava vertailu tulostaa viestin "Ehdottomasti totta!".

Jotta vertailussa otettaisiin huomioon myös muuttujan sen hetkinen tyyppi, käytetään kolmatta yhtäsuuri kuin -merkkiä. Jotta muuttujan tyypin saa otettua huomioon, tulee yhtäsuuruusvertailu toteuttaa kolmella yhtäsuuri kuin -merkillä (===).

Sama pätee myös erisuuri kuin (!=) -vertailulle.

Toistolauseet

JavaScriptissä on käytössä for- ja while-toistolauseet.

Funktiot, parametrit, ja arvon palauttaminen

Funktioille voi antaa arvoja, joita voidaan käyttää osana funktion lähdekoodia. Tämä on kätevää erityisesti silloin, kun samanlaista toiminnallisuutta tehdään useassa paikassa: toiminnallisuudesta voi tehdä funktion, jota voi kutsua. Funktiot voivat myös palauttaa arvon, joka asetetaan muuttujaan -- muuttujaa taas voidaan käyttää osana muuta ohjelmakoodia.

Funktioiden parametrit toimivat seuraavasti:

function tulostaViesti(viesti) {
    alert(viesti);
}

Kun ylläolevaa funktiota kutsutaan, sille tulee antaa parametri. Parametrille luodaan funktiokutsussa oma muuttuja viesti, johon parametrin arvo kopioidaan. Funktiota tulostaViesti suoritettaessa funktiolle alert annetaan aina sen arvon kopio, jonka funktio tulostaViesti saa parametrina.

Funktiot voivat myös palauttaa arvon. Arvo palautetaan komennolla return, jota seuraa palautettava arvo. Seuraavassa esimerkissä on funktio kysyNumeroJaTarkita kutsuu komentoa return jos käyttäjän syöttämä arvo ei ole numero. Jos komennolle return ei anna palautettavaa arvoa, funktiokutsusta poistutaan ilman arvon palauttamista.

function kysyNumeroJaTarkista() {
    var syote = prompt("Kirjoita numero");

    if(isNaN(Number(syote))) {
        alert("Et kirjoittanut numeroa!");
        return;
    }

    numero = parseInt(syote);

    if (numero == 5 || numero == 7) {
        alert("Oikein meni!");
    }
}

Muuttujien näkyvyys

JavaScriptissä muuttujilla on kaksi näkyvyystyyppiä: paikallinen ja globaali. Paikalliset muuttujat ovat olemassa vain siinä funktiossa missä ne on esitelty. Globaalit muuttujat ovat olemassa kaikkialla. Muuttujia voi esitellä ilman määrettä var, jolloin ne ovat globaaleja. Tämä johtaa ennen pitkää kaaokseen. Testaa alla olevia koodeja, ja huomaa niiden ero!

Käytä avainsanaa var aina kun esittelet muuttujan.

Taulukot

Taulukkotyyppisiä muuttujia voidaan luoda komennolla new Array(), jolle annetaan parametrina taulukon koko. Esimerkiksi seuraavassa luodaan 3 paikkaa sisältävä taulukko. Taulukkomuuttujan indeksointi tapahtuu hakasuluilla.

var salasanat = new Array(3);
salasanat[0] = "salasana";
salasanat[1] = "alasanas";
salasanat[2] = "lasanasa";

Jos taulukon arvot tiedetään ennalta, voidaan taulukko luoda myös suoraan konstruktorikutsussa.

var salasanat = new Array("salasana", "alasanas", "lasanasa");

Taulukot voivat sisältää myös erityyppisiä muuttujia.

var tiedot = new Array("Mikke", 1984);

Oliot

Taulukoiden lisäksi JavaScriptissä tiedon käsittelyyn käytetään Object-tyyppisiä muuttujia, eli olioita. Olio luodaan komennolla new Object(), jonka jälkeen oliolle voi asettaa arvoja.

var mikke = new Object();
mikke.nimi = "Mikke";
mikke.syntymavuosi = 1984;

Olioiden luontiin liittyy myös hieman kevyempi syntaksi, joka saattaa olla joillekin palvelinohjelmointi-kurssilla olleille tutun näköinen.

var mikke = {nimi: "Mikke", syntymavuosi: 1984};

Olioiden rakenne ei ole ennalta määrätty. Voimme esimerkiksi luoda muuttujalle mikke vaimon helposti.

var mikke = {nimi: "Mikke", syntymavuosi: 1984};

/* miken vaimo on ikinuori */
var kate = {nimi: "Kate", syntymavuosi: new Date().getFullYear() - 21};

mikke.vaimo = kate;

/* nyt voimme kysyä miken vaimon nimeä seuraavasti */
alert(mikke.vaimo.nimi);

Web-sivun elementtien arvojen käsittely

Palataan web-maailmaan. JavaScriptiä käytetään ennenkaikkea dynaamisen toiminnallisuuden lisäämiseksi web-sivuille. Esimerkiksi web-sivuilla oleviin elementteihin tulee pystyä asettamaan arvoja. JavaScriptissä pääsee käsiksi dokumentissa oleviin elementteihin komennolla document.getElementById("tunnus"), joka palauttaa elementin, jonka id-attribuutti on "tunnus".

Alla olevassa esimerkissä on luotu tekstikenttä, jonka HTML-koodi on <input type="text" id="tekstikentta"></input>. Kentän tunnus on siis tekstikentta. Javascriptillä on komento document.getElementById("tunnus"), jonka avulla voidaan hakea tietyn nimistä elementtiä. Tekstikenttäelementillä on attribuutti value, joka voidaan tulostaa.

Tekstikentälle voidaan asettaa arvo kuten muillekin muuttujille. Alla olevassa esimerkissä haetaan edellisen esimerkin tekstikenttä, ja asetetaan sille arvo 5.

Tehdään vielä ohjelma, joka kysyy käyttäjältä syötettä, ja asettaa sen yllä olevan tekstikentän arvoksi.

Arvon asettaminen osaksi tekstiä

Yllä tekstikentälle asetettiin arvo sen value-attribuuttiin. Kaikilla elementeillä ei ole value-attribuuttia, vaan joillain näytetään niiden elementin sisällä oleva arvo. Elementin sisälle asetetaan arvo muuttujaan liittyvällä attribuutilla innerHTML. Tässä alapuolella on esimerkiksi p-elementti, jonka id on js-hidden-p-element, ja jolla ei ole mitään arvoa.

Vastaavasti tekstin keskelle voi myös asettaa arvoja. Elementti span on tähän aivan mainio. Tämä teksti on span-elementin sisällä, jonka tunnus on "spanelementti".

Case: Laskin

Luodaan oma laskin. Laskimella on kaksi toiminnallisuutta: pluslasku ja kertolasku. Luodaan ensin laskimelle javascriptkoodi, joka on tiedostossa laskin.js. Javascript-koodissa oletetaan, että on olemassa input-tyyppiset elementit tunnuksilla "eka" ja "toka" sekä span-tyyppinen elementti tunnuksella "tulos". Funktiossa plus haetaan elementtien "eka" ja "toka" arvot, ja asetetaan pluslaskun summa elementin "tulos" arvoksi. Kertolaskussa tehdään lähes sama, mutta tulokseen asetetaan kertolaskun tulos. Koodissa on myös apufunktio, jota käytetään arvojen muuttamiseksi numeroiksi.

function haeNumero(tunnus) {
    return parseInt(document.getElementById(tunnus).value);
}

function asetaTulos(tulos) {
    document.getElementById("tulos").innerHTML = tulos;
}

function plus() {
    asetaTulos(haeNumero("eka") + haeNumero("toka"));
}

function kerto() {
    asetaTulos(haeNumero("eka") * haeNumero("toka"));
}

Laskimen käyttämä HTML-dokumentti näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title>Laskin</title>
    </head>
    <body>
        <header>
            <h1>Plus- ja Kertolaskin</h1>
        </header>

        <section>
            <p>
                <input type="text" id="eka" value="0"/ >
                <input type="text" id="toka" value="0" />
            </p>

            <p>
                <input type="button" value="+" onclick="plus();" />
                <input type="button" value="*" onclick="kerto();" />
            </p>


            <p>Laskimen antama vastaus: <span id="tulos"></span></p>
        </section>

        <script src="laskin.js"></script>
    </body>
</html>

Laskin itsessään näyttää seuraavalta:

Plus- ja Kertolaskin

Laskimen antama vastaus:

Laskimen jatkokehitys

Yllä oleva laskin tulee mukana tehtäväpohjassa. Tehtävänäsi on ensin lisätä laskimelle myös miinus- ja jakolaskut. Kun miinus- ja jakolasku toimii, integroi visualisoinnista tykkäävän kaverisi alla oleva koodi osaksi laskimen toimintaa. Alla oleva koodi lisää laskimeen piirtoelementin, sekä piirtää ruudulle liikkuvan neliön, jonka vauhti riippuu tuloksesta.

        <section>
            <canvas id="alusta" width="300" height="200" ></canvas>
        </section>
window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       || 
           window.webkitRequestAnimationFrame || 
           window.mozRequestAnimationFrame    || 
           window.oRequestAnimationFrame      || 
           window.msRequestAnimationFrame     || 
           function(/* kutsuttava funktio */ callback, /* elementti */ element){
               window.setTimeout(callback, 1000 / 60);
           };
    })();
  

function ajasta() {
    piirra();
    requestAnimFrame( ajasta );
}

function piirra() {
    var piirturi = document.getElementById("alusta").getContext("2d");
    var nopeus = parseInt(document.getElementById("tulos").innerHTML);
    if(!nopeus) {
        nopeus = 2;
    }
    
    if(nopeus < 0) {
        nopeus = 1 / (nopeus * -1);        
    }
    
    var aika = new Date().getTime() * 0.001 * nopeus;
    var x = Math.sin( aika ) * 100 + 125;
    
    //  piirretään ensin valkoinen tausta
    piirturi.fillStyle = "rgb(255, 255, 255)";
    piirturi.fillRect( 0, 0, 300, 200 );
    
    // ja sitten neliö
    piirturi.fillStyle = "rgb(255,0,0)";
    piirturi.fillRect(x, 50, 50, 50);
}

ajasta();
      

Kun olet valmis, ja sivun ladatessa näet liikkuvan punaisen neliön, jonka vauhti riippuu laskimen tuloksesta, palauta tehtävä TMC:lle.

Case: Tyylien muuttaminen JavaScriptillä

JavaScriptiä voi käyttää myös tyylien muokkaamiseen. Attribuutin class arvoa voi muuttaa dokumentista saatavan olion attribuutilla className. Tehdään vielä esimerkki, jossa sivulla oleva tieto vaihtuu linkkiä klikkaamalla, mutta selain ei oikeasti siirry sivulta toiselle. Luodaan ensiksi HTML-dokumentti, jossa on valmiiksi paikat tyylitiedostolle style.css ja lähdekoodille script.js.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title></title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body onload="init();" >
        <header>
            <h1>Kindler</h1>
            
            <nav>
                <!-- komento return false; estää selaimen siirtymisen toiselle sivulla -->
                <a href="#" onclick="displayArticle(0);return false;">Eka artikkeli</a>
                <a href="#" onclick="displayArticle(1);return false;">Toka artikkeli</a>
            </nav>
        </header>

        <article>
	  <h1>Eka artikkeli</h1>

	  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
	</article>
	
        <article>
	  <h1>Toka artikkeli</h1>

	  <p>Morbi a elit enim, sit amet iaculis massa. Vivamus blandit...</p>
	</article>

        <article>
	  <h1>Kolmas artikkeli</h1>

	  <p>Now that we know who you are, I know who I am. I'm...</p>
	</article>


        <script src="script.js"></script>
    </body>
</html>

Luodaan seuraavaksi sivulle tyylitiedosto. Määritellään article-elementille tyyliluokka "hidden": jos article-elementillä on tyyliluokka hidden, sitä ei näytetä selaimessa.

article.hidden {
    display: none;
}

Luodaan seuraavaksi JavaScript-toiminnallisuus. Emme käsittele sivua tunnusten avulla, vaan käytämme elementtien läpikäyntiin niiden tyyppejä. Haluamme käytännössä käsitellä kaikkia sivulla olevia article-elementtejä. Tähän on kätevä komento document.getElementsByTagName("elementinNimi"), joka palauttaa taulukon elementeistä, joiden elementin nimi on "elementinNimi". Haluamme myös, että kun sivu on ladattu, näytetään vain ensimmäinen artikkeli. Tätä varten body-elementille on olemassa attribuutti onload, jolle voi määritellä funktion nimen, jota kutsutaan kun sivun lataaminen on valmis.

function init() {
    displayArticle(0);
}

function displayArticle(index) {
    var articles = document.getElementsByTagName("article");

    for(var i = 0; i < articles.length; i++) {
        if (index == i) {
            articles[i].className='';
        } else {
            articles[i].className='hidden';
        }
    }
}

Voit tutustua valmiin sivun toiminnallisuuteen täällä.

PerusMOOC (3p)

Huom! Tämä tehtävä on kolmen pisteen arvoinen: palauta se vain jos teet tehtävän kokonaan.

Tehtävässä on käytetty seuraavia värejä:

Fonttien määrittely on muotoa

    font-family: 'Trebuchet MS', Trebuchet, Arial, sans-serif;

Tehtävän mukana tuleva sivu näyttää seuraavanlaiselta:

Tehtävänäsi on ensin lisätä sivulle tyylitiedosto, jonka avulla sivusta tulee seuraavannäköinen. Kun lisäät tyylitiedostoa, lisää valikkoon myös toiminnallisuus, jonka avulla linkin tausta muuttuu kun hiiri on sen päällä.

Kun tyylit ovat valmiit, lisää sivulle toiminnallisuus, jossa vain ensimmäinen osio näkyy ensin. Linkkejä klikkaamalla sivulla vaihdetaan osiosta toiseen. Alla olevassa kuvassa osiota "Materiaali" on juuri klikattu.

Kun olet valmis, ja sivusi näyttää oikealta Chromessa, palauta tehtävä TMC:lle.

DOM

DOM (Document Object Model) on ohjelmointirajapinta HTML (ja XML) -dokumenttien rakenteen ja sisällön muokkaamiseksi. Se sisältää kuvauksen HTML-dokumentin elementeistä ja niiden asemoinnista dokumentissa. HTML-dokumentti kuvataan usein puumaisena tietorakenteena, jossa jokainen sivun elementti on puun solmu (oksa) tai lehti (solmu, josta ei lähde oksia). Jokaisella elementillä on myös nimi, jolla siihen pääsee käsiksi.

Suurin osa nykyaikaisista web-selaimista toteuttaa W3C DOM-standardin, sekä usein tarjoavat omia lisävälineitä dokumenttien muokkaamiseen. W3C DOM-standardi sisältää esimerkiksi aiemmin käyttämämme kutsun document.getElementById("tunnus"), jonka avulla päästään käsiksi dokumentin sisältämään elementtiin, jonka attribuutin id arvo on "tunnus".

HTML-dokumentin elementit ja niihin liittyvät ominaisuudet (attribuutit, tapahtumat, ...) on jäsennelty erilaisiin olioihin. Osaan pääsee käsiksi suoraan. Esimerkiksi window-oliolla päästään käsiksi mm. selainikkunassa tapahtuviin tapahtumiin (esim. näppäimistön kuuntelu), document-olio taas liittyy HTML-dokumenttiin ja sen sisältämiin elementteihin. Kaikkiin dokumentin elementteihin pääsee käsiksi document-elementin kautta.

Esimerkiksi dokumentissa olevan canvas-elementin saa haettua siihen liittyvällä tunnuksella document-oliota käyttäen. Mozillan sovelluskehittäjien sivustolla on hyvä kuvaus elementteihin liittyvistä rajapinnoista, kts. https://developer.mozilla.org/en-US/docs/Gecko_DOM_Reference#HTML_element_interfaces.

DOM-standardi sisältää useita tasoja, eli versioita. Taso 1 sisältää mm. dokumentin elementtien luomisen sekä hakemisen getElementsByTagName-komennolla. Kutsu getElementsByTagName attribuutilla * palauttaa listan, joka sisältää kaikki sivun elementit. Lähes kaikki nykyään käytössä olevat selaimet tukevat tason 1 toiminnallisuutta. Taso 2 sisältää mm. tuen dokumentin tyylien muokkaamiseen DOM-puun kautta sekä erilaisten tapahtumien (mm. hiiri, näppäimistö) käsittelyä tukevan järjestelmän. Taso 3 laajentaa tason 2 toiminnallisuutta mm. dokumentin elementtien ja tapahtumien käsittelyssä.

DOM-spesifikaatio sisältää useita eri komponentteja. Alla kuva DOM-arkkitehtuurista.

W3C työskentelee tällä hetkellä (1.11.2012) tason 4 spesifikaation kanssa. Taso 4 tulee olemaan laajennus aiempiin tasoihin, joka tarjoaa mm. selvennyksiä tapahtumien käsittelyyn ja lisätoiminnallisuuksia dokumentin identifiointiin.

Selaintuki

Uusimpien DOM-spesifikaatioiden ja lisäosien tuki on rikkinäinen osassa selaimia. Selainvalmistajat eivät usein kiinnitä huomiota vanhempiin selainversioihin, jolloin uudet ominaisuudet ovat käytössä vasta uudemmissa selaimissa. Sivusto "Can I Use..." (http://caniuse.com on yksi monista sivustoista, jotka tarjoavat spesifikaatioiden ja lisäosien yhteensopivuuslistauksia eri selaimille.

Elementtien valinta

Olemme käyttäneet dokumentin getElementById-kutsua tietyn elementin hakemiseen. Kaikki sivun elementit voi taas hakea esimerkiksi getElementsByTagName("*")-kutsulla. Molemmat ovat kuitenkin hieman kömpelöjä jos tiedämme mitä haluamme hakea verrattuna esimerkiksi CSS:n käyttämään elementtien valintatyyliin (kts. http://www.w3.org/TR/selectors/#selectors.

W3C DOM-määrittely sisältää myös paremman ohjelmointirajapinnan elementtien läpikäyntiin. Selectors API sisältää mm. querySelector-kutsun, jolla saadaan CSS-valitsinten kaltainen kyselytoiminnallisuus.

Selector APIn tarjoamien querySelector (yksittäisen osuman haku) ja querySelectorAll (kaikkien osumien haku) -komentojen avulla kyselyn rajoittaminen vain header-elementissä oleviin a-elementteihin on helppoa.

var linkit = document.querySelectorAll("nav a");
// linkit-muuttuja sisältää nyt kaikki a-elementit, jotka ovat nav-elementin sisällä

Vastaavasti header-elementin sisällä olevat linkit voi hakea seuraavanlaisella kyselyllä.

var linkit = document.querySelectorAll("header a");
// linkit-muuttuja sisältää nyt kaikki a-elementit, jotka ovat header-elementin sisällä

Myös tietyn luokan toteuttavien elementtien haku on helppoa. Alla olevassa esimerkissä on kolme tekstikenttää, joista 2 on piilotettu. Piilotettujen tekstikenttien tyyliluokka on dom-esim-1-hidden.

text 1

text 2

text 3

Voimme hakea querySelectorin avulla myös elementtejä, joilta puuttuu tietty ominaisuus. Alla haemme kaikki tyyliluokan dom-esim-2 toteuttavan elementin sisällä olevat p-elementit, joilla ei ole tyyliluokkaa dom-esim-2-hidden. Lopuksi lisäämme kyselyssä löydetyille elementeille tyyliluokan dom-esim-2-hidden, jolloin elementit piilotetaan.

Alla olevan sivun lähdekoodi on seuraavanlainen (tyyliluokkien oudot nimet johtuvat tämän dokumentin rakenteesta -- haluamme että esimerkit eivät vaikuta toisiin esimerkkeihin).

  <article class="dom-esim-2">
    <p class="dom-esim-2-hidden">text 1</p>
    <p>text 2</p>
    <p class="dom-esim-2-hidden">text 3</p>
  </article>

text 1

text 2

text 3

Mitä käy jos poistat ylläolevasta kyselystä alkuosan dom-esim-2 ja suoritat kyselyn? Pohdi ennen kokeilemista!

Elementtien lisääminen

HTML-dokumenttiin lisätään uusia elementtejä document-olion createElement-metodilla. Esimerkiksi alla luodaan p-elementti, joka asetetaan muuttujaan tekstiElementti. Tämän jälkeen luodaan tekstisolmu, joka sisältää tekstin "o-hai". Lopulta tekstisolmun lisätään tekstielementtiin.

var tekstiElementti = document.createElement("p");
var tekstiSolmu = document.createTextNode("o-hai");

tekstiElementti.appendChild(tekstiSolmu);

Ylläoleva esimerkki ei luonnollisesti muuta HTML-dokumentin rakennetta sillä uutta elementtiä ei lisätä osaksi HTML-dokumenttia. Olemassaoleviin elementteihin voidaan lisätä sisältöä elementin appendChild-metodilla. Alla olevan tekstialue sisältää article-elementin, jonka tunnus on dom-esim-3. Voimme lisätä siihen elementtejä elementin appendChild-metodilla.

Artikkelielementin sekä sen sisältämien tekstielementtien lisääminen onnistuu vastaavasti. Alla olevassa esimerkissä käytössämme on seuraavanlainen section-elementti.

<!-- .. dokumentin alkuosa .. -->
    <section id="dom-esim-4"></section>
<!-- .. dokumentin loppuosa .. -->

Uusien artikkelien lisääminen onnistuu helposti aiemmin näkemällämme createElement-metodilla.

DOM Walker

Luo tehtäväpohjaan koodi, joka käy läpi kaikki sivun body-elementin sisällä olevat elementit, ja tulostaa niiden tägien nimet elementtiin, jonka tunnus on "dom". Älä tulosta "dom"-tunnuksella varustettuun elementtiin "dom"-tunnuksellista elementtiä tai sen sisällä olevia elementtejä. Koodi tulee suorittaa kun sivu on ladattu.

Tehtäväpohjan mukana olevan sivun kanssa koodi tulostaa sivun loppuun seuraavanlaisen listan elementtejä. Voit käyttää p-elementtiä tägien nimien erotteluun.

HEADER

H1

NAV

UL

LI

A

LI

A

SECTION

HEADER

H1

ARTICLE

P

SECTION

HEADER

H1

ARTICLE

P

SCRIPT

Kun sovellus toimii toivotusti, palauta se TMC:lle.

Elementtien poistaminen

Dokumentin puumaisen rakenteen takia elementin lisääminen tapahtuu elementin vanhempaan liittyvällä appendChild-metodilla. Koska elementin vanhempi pitää kirjaa kaikista sen lapsista, tulee elementti myös poistaa sen vanhemman kautta.

DOM-puun elementtien toteuttamat metodit sisältävät metodin removeChild, jota voi käyttää lapsielementin poistamiseen. Alla olevassa esimerkissä haluamme poistaa elementin, jonka tunnus on "poistettava".

// myös document.getElementById("poistettava") käy
var poistettava = document.querySelector("#poistettava");
poistettava.parentNode.removeChild(poistettava);
<!-- .. dokumentin alkuosa .. -->
    <article id="poistettava">
        <p>Lorem Ipsum jne..</p>
    </article>
<!-- .. dokumentin loppuosa .. -->

Lorem Ipsum jne..

Yllä olevan koodin suorituksen jälkeen elementti on poistettu DOM-puusta. Nykyaikaisissa selaimissa oleva roskienkeruumekanismi poistaa myös poistettavien elementtien lapsielementit.

Tapahtumien käsittely

Tason 2 DOM-spesifikaatiossa määriteltiin tuki tapahtumien käsittelylle, kts. http://www.w3.org/TR/DOM-Level-2-Events/. Tason 3 spesifikaatio ei ole vielä valmis, mutta sen viimeisin versio löytyy täältä).

Jokaisella tapahtumalla on kohde, johon se liittyy. Kun sivuilla tapahtuu tapahtuma, esimerkiksi hiirellä klikataan nappia, DOM-toteutus ohjaa tapahtuman napin tapahtumankuuntelijalle. Jos napille on rekisteröity tapahtumankuuntelija, suoritetaan tapahtumankuuntelijan koodi. Tapahtumien käsittelyä varten sivuille tulee määritellä funktioita. Olemme aiemmin määritelleet tapahtumien sattuessa kutsuttavat osana HTML-dokumenttia seuraavasti.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title></title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body onload="init();" >
        <header>
            <h1>Kindler</h1>
            
            <nav>
                <!-- komento return false; estää selaimen siirtymisen toiselle sivulla -->
                <a href="#" onclick="displayArticle(0);return false;">Eka artikkeli</a>
                <a href="#" onclick="displayArticle(1);return false;">Toka artikkeli</a>
            </nav>
        </header>

        <!-- muu sisältö -->

        <!-- lähdekooditiedostojen lataus -->
    </body>
</html>

Vastuiden erottamisen näkökulmasta yllä oleva lähestymistapa on väärä. Haluamme eriyttää sovelluslogiikan käyttöliittymästä. Ainoa HTML-dokumentissa sallittu JavaScript-kutsu on body-elementin onload-attribuutille määriteltävä kutsu, joka suoritetaan kun sisältö on ladattu.

Yksi tapa poistaa ylläolevassa dokumentissa olevat JavaScript-kutsut on lisätä init-metodiin tapahtumankäsittelijöiden lisääminen. Käytetään aiemmin oppimaamme querySelector-toteutusta siihen, että lisäämme tapahtumankäsittelijät vain menuvalikon linkkeihin. Jotta saisimme tapahtumankäsittelijän toimimaan oikein a-elementissä, meidän tulee myös kieltää linkin seuraaminen. Tämä onnistuu tapahtumaan liittyvällä kutsulla preventDefault().

function init() {
    var navLinks = document.querySelectorAll("header nav a");
    for(var i = 0; i < navLinks.length; i++) {
        var link = navLinks[i];

        // lisätään elementille id, josta päätellään näytettävä artikkeli
        link.id = i;
    
        link.onclick = function(eventInformation) {
            var origin = eventInformation.target;

            // kutsutaan erillistä displayArticle-funkiota, joka
            // näyttää halutun artikkelin
            displayArticle(origin.id);

            // kielletään selainta tekemästä oletustoiminto (siirtyminen)
            eventInformation.preventDefault();
        }
    }

    // ...
}

Nyt aiempi sivumme toimii myös seuraavannäköisenä.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title></title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body onload="init();" >
        <header>
            <h1>Kindler</h1>
            
            <nav>
                <a href="#">Eka artikkeli</a>
                <a href="#">Toka artikkeli</a>
            </nav>
        </header>

        <!-- muu sisältö -->

        <!-- lähdekooditiedostojen lataus -->
    </body>
</html>

Oikeastaan yllä käyttämämme lähestymistapa on myös hieman hölmö. Kun määrittelemme onclick-funktion, korvaamme aiemman funktion. Fiksumpaa olisi lisätä uusi funktio aiempien lisäksi. Tämä onnistuu elementteihin liittyvällä metodilla addEventListener. Metodille addEventListener määritellään tapahtuman nimi (esim click, huomaa ero!), funktio jota kutsutaan (joko funktion nimi tai konkreettinen toteutus), ja totuusarvoinen muuttuja, jolla kerrotaan tuleeko muun dokumentin reagoida tapahtumaan. Tällöinkin tapahtumaan reagoivat vain elementit, jotka ovat tapahtuman laukaisevan elementin vanhempia.

function init() {
    var navLinks = document.querySelectorAll("header nav a");
    for(var i = 0; i < navLinks.length; i++) {
        var link = navLinks[i];

        // lisätään elementille id, josta päätellään näytettävä artikkeli
        link.id = i;

        // lisätään tapahtumankuuntelija tapahtumalle click. huom! ero onclick-attribuuttiin
        link.addEventListener('click', function(eventInformation) {
            var origin = eventInformation.target;

            // kutsutaan erillistä displayArticle-funkiota, joka
            // näyttää halutun artikkelin
            displayArticle(origin.id);

            // kielletään selainta tekemästä oletustoiminto (siirtyminen)
            eventInformation.preventDefault();
        }, false);
    }

    // ...
}

Tapahtumankäsittelyyn liittyvän funktion voi määritellä myös erikseen, jolloin tapahtumankäsittelijää lisättäessä funktioon viitataan sen nimellä.

function handleLinkClick(eventInformation) {
    var origin = eventInformation.target;

    // kutsutaan erillistä displayArticle-funkiota, joka
    // näyttää halutun artikkelin
    displayArticle(origin.id);

    // kielletään selainta tekemästä oletustoiminto (siirtyminen)
    eventInformation.preventDefault();
}

function init() {
    var navLinks = document.querySelectorAll("header nav a");
    for(var i = 0; i < navLinks.length; i++) {
        var link = navLinks[i];

        // lisätään elementille id, josta päätellään näytettävä artikkeli
        link.id = i;

        // lisätään tapahtumankuuntelija tapahtumalle click. huom! ero onclick-attribuuttiin
        link.addEventListener('click', handleLinkClick, false);
    }

    // ...
}

Lisää eri tapahtumatyypeistä ja tapahtumankäsittelystä löydät esim. täältä.

PerusMOOC, jatkoa

Tehtäväpohjassa on viime viikon loppupuolelta alustava PerusMOOC-sivu. Sivulla on kuitenkin vielä ongelma: html-sivu sisältää JavaScriptiä enemmän kuin on sallittu.

Muuta sivustoa siten, että ainoa index.html-sivun JavaScript-kutsu on body-elementin onload-attribuutille asetettu init();-funktiokutsu. Sivun toiminnallisuuden tulee pysyä ennallaan.

Varaudu myös siihen, että linkeille saatetaan asettaa myöhemmin toteutettavissa koodeissa uusia tapahtumankäsittelijöitä.

Kun sovellus toimii toivotusti, palauta se TMC:lle.

Lisää JavaScriptistä

Käydään läpi hieman tarkemmin JavaScriptiin liittyviä mielenkiintoisuuksia, sekä tutustutaan hyviin ohjelmointikäytänteisiin.

Muuttujien näkyvyys

Olemme aiemmin todenneet, että JavaScriptissä muuttujilla on kaksi eri näkyvyystyyppiä, paikallinen ja globaali. Paikallisella näkyvyydellä tarkoitetaan että muuttujat ovat olemassa vain funktion sisällä, ja globaalilla sitä, että muuttujat ovat näkyvissä kaikkialla. Kun muuttuja määritellään var-etuliitteellä, on se olemassa funktiossa, jossa se on määritelty.

Mitä tarkoittaa "muuttuja on olemassa funktiossa, jossa se on määritelty"? Pohdi seuraavaa ohjelmaa ja päättele mitä tapahtuu kun painat "Suorita koodi!"-nappia.

Uskoisimme, että et arvannut lopputulosta, jollet tuntenut JavaScriptiä ennalta.

Käytännössä JavaScript siirtää muuttujien määrittelyt funktion alkuun, mutta muuttujien arvon asetus tapahtuu alkuperäisen koodin määrittelemässä kohdassa. Ylläoleva koodi tulkitaan JavaScript-tulkin toimesta seuraavasti.

var summa = 21;

function mitaKay() {
    var summa;    

    if (false) {
        summa = 42;
    }

    alert(summa);
}

mitaKay();

Koska ylläolevassa ohjelmakoodissa funktion mitaKay sisällä määritellylle muuttujalle summa ei koskaan aseteta arvoa, on sen arvo undefined.

Olio-ohjelmointi ja prototyyppimalli

Olio-ohjelmoinnissa kyse on ennen kaikkea ongelma-alueen käsitteiden mallintamisesta osana ohjelmakoodia. Olio-ohjelmoinnissa ohjelmoijat pyrkivät puhumaan samoilla käsitteillä kuin ohjelmistoa tilaavat asiakkaat. Käsitteet liittyvät maailmassa olevaan dataan, ja ovat interaktiossa toisten käsitteiden kanssa olioihin liittyvien metodien kautta.

Kommunikaation ja käsitemaailman helpottamisen lisäksi olio-ohjelmoinnissa ohjelma hajoitetaan hallittaviksi osiksi, jotka kapseloivat pienempää toiminnallisuutta. Kukin olio voi sisältää dataa sekä lähettää ja vastaanottaa informaatiota.

JavaScriptissä ei ole mm. Javasta tuttuja luokkia, vaan uusien olioiden luominen tapahtuu funktiokutsuilla. JavaScript-kielen oliomalli perustuu funktioihin, joiden prototyyppeihin voidaan liittää uusia funktioita.

Tutkitaan hieman erilaisia koodiesimerkkejä.

Luodaan uusi olio new Object()-kutsulla. Oliolle asetetaan muuttujat nimi ja ika.

var mikke = new Object();
mikke.nimi = "Michael Knight";
mikke.ika = 17;

alert(mikke.nimi);

Ylläoleva ohjelmakoodi vastaa allaolevaa olion luomista.

var mikke = {nimi: "Michael Knight", ika: 17};
alert(mikke.nimi);

Olion muuttujiin pääsee käsiksi myös seuraavanlaisen notaation avulla. Notaatio saattaa olla tuttu mikäli on aiemmin ohjelmoinut esimerkiksi PHP:llä tai perlillä.

var mikke = {nimi: "Michael Knight", ika: 17};
alert(mikke["nimi"]);

Käytännössä oliot JavaScriptissä ovat kokoelmia avain-arvo -pareja. Jokaisella oliolla voi olla omanlaisensa avaimet ja niiden arvot. Kaikki JavaScriptin oliot laajentavat valmista Object-oliota.

Omien olioiden luonti

Omien olioiden luonti tapahtuu funktioiden avulla. Huomaa heti alkuun hyvä nimeämiskäytäntö: funktiot, joita käytetään olioiden luomiseen nimetään isolla alkukirjaimella. Luodaan funktio Opiskelija, jota käytetään opiskelija-olioiden luomiseen. Opiskelijalla on kaksi attribuuttia: nimi ja opintopisteet.

function Opiskelija(nimi) {
    this.nimi = nimi;
    this.opintopisteet = 0;
}

Huomaa ylläolevan määrittelyn määre this. Määreellä this kerrotaan, että käsitellyn muuttujan arvo liittyy juuri tähän olioon. Kun funktio Opiskelija on määritelty, voimme luoda uusia opiskelijaolioita seuraavasti.

var mikke = new Opiskelija("Michael");
var kasper = new Opiskelija("Casper");
kasper.opintopisteet = 400;

alert("Nimi " + mikke.nimi + ", noppia: " + mikke.opintopisteet);
alert("Nimi " + kasper.nimi + ", noppia: " + kasper.opintopisteet);

Ohjelma näyttää pop-up -ikkunat, joissa on viestit

Nimi Michael, noppia: 0
Nimi Casper, noppia: 400

Jokaisella funktiolla on prototyyppi, joka sisältää tiedon funktioon liittyvistä attribuuteista. Prototyypin kautta lisättävien funktioiden avulla pääsemme käsiksi olioiden this-viitteeseen, mikä mahdollistaa olion sisäisen tilan muuttamisen.

Koska attribuutit voivat olla myös funktioita, funktion prototyypille voidaan määritellä uusia metodeja, joilla olion tilaa voidaan muokata. Lisätään funktiolle Opiskelija prototyyppifunktio opiskeleYksin, joka kasvattaa opintopisteiden määrää yhdellä.

Opiskelija.prototype.opiskeleYksin = function() {
    this.opintopisteet++;
}
var mikke = new Opiskelija("Michael");
var kasper = new Opiskelija("Casper");
kasper.opintopisteet = 400;

mikke.opiskeleYksin();

alert("Nimi " + mikke.nimi + ", noppia: " + mikke.opintopisteet);
alert("Nimi " + kasper.nimi + ", noppia: " + kasper.opintopisteet);

Nyt näemme ylläolevalla koodilla pop-up -ikkunat, joissa on viestit

Nimi Michael, noppia: 1
Nimi Casper, noppia: 400

Lisätään funktiolle Opiskelija vielä toinen prototyyppifunktio opiskeleYhdessa, joka saa parametrina toisen opiskelijan. Voimme tarkistaa että parametrina saatu muuttuja on jotain tiettyä tyyppiä instanceof-vertailuoperaatiolla. Viitettä this voi käyttää olioon liittyvien funktioiden kutsumisessa.

Opiskelija.prototype.opiskeleYhdessa = function(kanssaOpiskelija) {
    if(!(kanssaOpiskelija instanceof Opiskelija)) {
        this.opiskeleYksin();
        return;
    }

    this.opintopisteet += 2;
    kanssaOpiskelija.opintopisteet += 2;
}
var mikke = new Opiskelija("Michael");
var kasper = new Opiskelija("Casper");
kasper.opintopisteet = 400;

mikke.opiskeleYksin();
mikke.opiskeleYhdessa(kasper);

mikke.opiskeleYhdessa("porkkana");

alert("Nimi " + mikke.nimi + ", noppia: " + mikke.opintopisteet);
alert("Nimi " + kasper.nimi + ", noppia: " + kasper.opintopisteet);
Nimi Michael, noppia: 4
Nimi Casper, noppia: 402

Oliolaskuri

Luo tehtäväpohjan mukana tulevalle HTML-sivulle toiminnallisuus, jossa nappia painettaessa sivulla näkyvässä tekstissä olevan numeron arvo kasvaa aina yhdellä. HTML-sivulle saa asettaa vain yhden JavaScript-kutsun, joka tulee body-elementin onload-attribuuttiin.

Toteuta apuvälineeksi olion luova funktio Laskin jolla on muuttuja luku. Lisää funktiolle Laskin funktiot kasvata, joka kasvattaa olion luvun arvoa yhdellä, ja annaLuku, joka palauttaa olion luvun arvon.

Varaudu siihen, että napille voidaan asettaa myöhemmin uusia tapahtumankäsittelijöitä.

Kun sovellus toimii toivotusti, palauta se TMC:lle.

Esimerkki: Kurssikirjanpito

Luodaan Opiskelijan lisäksi vielä funktiot Kurssin ja Kurssisuorituksen luomiseen. Kurssilla on nimi ja opintopistemäärä, kurssisuoritus sisältää viitteen suoritettuun kurssiin ja opiskelijaan sekä suorituspäivämäärän ja arvosanan. Suorituspäivämääränä käytetään suoritusolion luontipäivämäärää.

function Kurssi(nimi, opintopisteet) {
    this.nimi = nimi;
    this.opintopisteet = opintopisteet;
}

function Kurssisuoritus(opiskelija, kurssi, arvosana) {
    this.opiskelija = opiskelija;
    this.kurssi = kurssi;
    this.arvosana = arvosana;
    this.paivamaara = new Date();
}

Näiden lisäksi käytössämme on Kirjanpito, johon voi lisätä opiskelijoiden suorituksia. Kirjanpito tarjoaa nopean pääsyn kurssiin liittyviin suorituksiin ja opiskelijan suorituksiin. Alla oletetaan, että kurssien ja opiskelijoiden nimet ovat yksikäsitteiset.

function Kirjanpito() {
    // kaikki suoritukset
    this.kurssisuoritukset = new Array();

    // opiskelijakohtaiset suoritukset
    this.opiskelijanSuoritukset = {};

    // kurssikohtaiset suoritukset
    this.kurssinSuoritukset = {};
}

Kirjanpito.prototype.lisaaSuoritus = function(kurssi, opiskelija, arvosana) {
    var suoritus = new Kurssisuoritus(opiskelija, kurssi, arvosana);
    
    // metodilla push lisätään listaan
    this.kurssisuoritukset.push(suoritus);

    // erilliset metodit opiskelijakohtaisten ja kurssikohtaisten suoritusten
    // lisäämiseen
    this.lisaaOpiskelijanSuoritus(opiskelija.nimi, suoritus);
    this.lisaaKurssinSuoritus(kurssi.nimi, suoritus);
}

Kirjanpito.prototype.lisaaOpiskelijanSuoritus = function(opiskelijanNimi, suoritus) {
    // jos opiskelijan nimellä ei ole yhtäkään suoritusta, on nimellä
    // saatava arvo false -- luodaan tällöin lista suorituksille
    if(!this.opiskelijanSuoritukset[opiskelijanNimi]) {
        this.opiskelijanSuoritukset[opiskelijanNimi] = new Array();
    }

    this.opiskelijanSuoritukset[opiskelijanNimi].push(suoritus);
}

Kirjanpito.prototype.lisaaKurssinSuoritus = function(kurssinNimi, suoritus) {
    if(!this.kurssinSuoritukset[kurssinNimi]) {
        this.kurssinSuoritukset[kurssinNimi] = new Array();
    }

    this.kurssinSuoritukset[kurssinNimi].push(suoritus);
}

Kirjanpito.prototype.haeOpiskelijanKurssisuoritukset = function(opiskelijanNimi) {
    return this.opiskelijanSuoritukset[opiskelijanNimi];
}

Kirjanpito.prototype.haeKurssinSuoritukset = function(kurssinNimi) {
    return this.kurssinSuoritukset[kurssinNimi];
}

Kirjanpito.prototype.haeKaikkiSuoritukset = function() {
    return this.kurssisuoritukset;
}

Kirjanpidon käyttäminen on helpohkoa. Alla olevassa esimerkissä luodaan kurssi, opiskelija ja kirjanpito-olio, sekä lisätään suoritus kirjanpitoon. Tämän jälkeen kaikki suoritukset lisätään dokumentissa olevaan "data"-tunnuksella merkittyyn elementtiin. Käytössämme on myös apufunktio lisaaSuoritusteksti, joka lisää annettuun elementtiin tekstielementin, jossa on suorituksen tiedot.

function lisaaSuoritusteksti(elementti, suoritus) {
   var teksti = suoritus.kurssi.nimi + " " + suoritus.opiskelija.nimi + " " + suoritus.arvosana;
   elementti.appendChild(document.createTextNode(teksti));
}

var weso = new Kurssi("Web-selainohjelmointi");
var mikke = new Opiskelija("Michael");
var kirjanpito = new Kirjanpito();

kirjanpito.lisaaSuoritus(weso, mikke, 1);

var suoritukset = kirjanpito.haeKaikkiSuoritukset();
var data = document.getElementById("data");

for(var i = 0; i < suoritukset.length; i++) {
    var elementti = document.createElement("p");

    lisaaSuoritusteksti(elementti, suoritukset[i]);
    data.appendChild(elementti);   
}

Ylläolevaa sovellusta voisi käyttää hyvin myös käyttöliittymästä. Käytännössä tällöin HTML-dokumenttiin luotaisi lomake, jonka kautta kurssisuorituksia lisättäisiin. Näiden lisäksi todennäköisesti käytössä olisi omat lomakkeet kurssien ja opiskelijoiden lisäämiselle, jolloin kurssin ja opiskelijan voisi hakea kätevästi listasta.

Tavara, Matkalaukku, Ruuma

Jokaisella tavaralla on nimi ja paino. Matkalaukkuun lisätään tavaroita, ja matkalaukulla on maksimipaino. Ruumaan taas lisätään matkalaukkuja, ja myös ruumalla on maksimipaino. Matkalaukkuun voi lisätä vain tavaroita, ja ruumaan vain matkalaukkuja. Jos matkalaukun ja uuden tavaran yhteispaino on suurempi kuin matkalaukun maksimipaino, ei tavaraa voida lisätä. Vastaavasti ruumalle.

Toteuta olioita luovat funktiot Tavara, Matkalaukku, ja Ruuma lähdekooditiedostoon code.js. Voit käyttää alla olevaa (ja tehtäväpohjassa tulevaa) testikoodia toteutustesi testaamiseen.

var kivi = new Tavara("kivi", 3);
var kirja = new Tavara("kirja", 7);
var pumpuli = new Tavara("pumpuli", 0.001);

var laukku = new Matkalaukku(10);
var vuitton = new Matkalaukku(3);

var schenker = new Ruuma(15);


laukku.lisaa(kivi);
alert("laukun paino, pitäisi olla 3: " + laukku.paino());
laukku.lisaa(kivi); // virhe: "Tavara lisätty jo, ei onnistu!"

laukku.lisaa(kirja);
alert("laukun paino, pitäisi olla 10: " + laukku.paino());

laukku.lisaa(pumpuli); // virhe: "Liian painava, ei pysty!"

alert("laukun paino, pitäisi olla 10: " + laukku.paino());


schenker.lisaa(laukku);
schenker.lisaa(pumpuli); // virhe: Vääränlainen esine, ei onnistu!

alert("Ruuman paino, pitäisi olla 10: " + schenker.paino());

vuitton.lisaa(pumpuli);
schenker.lisaa(vuitton);
alert("Ruuman paino, pitäisi olla noin 10.001: " + schenker.paino()); 

pumpuli.paino = 300;
alert("Ruuman paino, pitäisi olla 310: " + schenker.paino()); // hups!

Palauta tehtävä TMC:lle kun se toimii toivotusti.

Moduulit

Kun tutkimme edellistä esimerkkiä tarkemmin, huomaamme että prototyyppien avulla määritelty oliotoiminnallisuus ei kapseloi olioiden muuttujia, vaan niihin pääsee käsiksi suoraan. Attribuuttien kapseloinnista on kuitenkin monia hyötyjä, joista tärkein lienee se, että olion sisäistä rakennetta voidaan muuttaa ilman että sitä käyttäviä sovelluksia tarvitsee muuttaa.

Ylläolevaa kirjanpitoa käyttävä ohjelmoija voisi epähuomiossa lisätä uusia kurssisuorituksia esimerkiksi suoraan kirjanpidon sisäiseen muuttujaan kurssisuoritukset, jolloin opiskelijakohtaisia suoritustietoja ei löytyisi nykyisellä toteutuksella.

var weso = new Kurssi("Web-selainohjelmointi");
var mikke = new Opiskelija("Michael");
var kirjanpito = new Kirjanpito();

kirjanpito.kurssisuoritukset.push(new Kurssisuoritus(weso, mikke, 1));

Kun ohjelmoijat käyttävät Kirjanpito-funktion määrittelemää ohjelmointirajapintaa, eli sen funktioita, on ohjelman laajentaminen helpompaa. Tällöin ei tarvitse huolehtia siitä, että esimerkiksi suorituspäivämäärän perusteella tapahtuva haku olisi rikki jo alusta lähtien koska joku käyttää valmista koodia väärin.

Eräs tapa tiedon kapselointiin on Module Pattern-suunnittelumallia. Ennen siihen tutustumista, tutustutaan kuitenkin anonyymeihin funktioihin ja sulkeumiin (Closure).

Anonyymit funktiot

Anonyymit funktiot ovat funktioita, joita ei kiinnitetä muuttujiin, jolloin ne eivät saa nimeä. Esimerkiksi seuraavassa ohjelmassa käytetään anonyymiä funktiota lukuvälin lukujen summan laskemiseen. Mielenkiintoista alla olevassa koodissa on se, että funktio on olemassa vain sen suorituksen ajan.

var lopputulos = (function(alku, loppu) {
                      var summa = 0;
                      for (var i = alku; i < loppu; i++) {
                          summa += i;
                      }
                      return summa;
                  })(1, 3);

alert(lopputulos); // 3

Sulkeumat

Sulkeumat ovat yleisesti ottaen lauseita, jotka voivat sisältää muuttujia, mutta jotka piilottavat muuttujat sisäänsä. Käytännössä sulkeumia luodaan luomalla funktioita funktion sisään, jolloin ulompi funktio kapseloi sisältönsä. Tutustutaan sulkeumiin hieman tarkemmin.

Olemme aiemmin huomanneet, että funktioiden sisällä määritellyt var -muuttujat eivät näy koko ohjelmalle, vaan ne ovat näkyvillä vain sen funktion sisällä, jossa ne on määritelty. Esimerkiksi seuraavassa funktiossa rajoitettuSumma funktio sisältää muuttujan summa, jota ei näy funktion ulkopuolelle. Kun funktio palauttaa arvon, se palauttaa kopion summa-muuttujan sisällöstä.

function rajoitettuSumma(alku, loppu) {
    var summa = 0;
    for (var i = alku; i < loppu; i++) {
        summa += i;
    }
    
    return summa;
}
var tulos = rajoitettuSumma(1, 2);
alert(tulos); // 1

Koska JavaScriptissä muuttujat voivat olla funktioita, voimme palauttaa funktiosta toisen funktion.

function pankki() {
    return function(alku, loppu) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        return summa;
    }
}

Kun käyttäjä kutsuu ylläolevaa funktiota, palauttaa funktio toisen funktion. Palautettu funktio toteuttaa aiemmin näkemämme funktion rajoitettuSumma toiminnallisuuden.

var funktio = pankki();
alert(funktio(1, 2)); // 1

Oikeastaan, funktion pankki sisälle voi luoda muuttujan rajoitettuSumma, jonka voimme palauttaa.

function pankki() {
    
    var rajoitettuSumma = function(alku, loppu) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        return summa;
    };

    return rajoitettuSumma;
}

Yllä oleva toiminnallisuus vastaa aiempaa funktiota. Muutetaan funktion rajoitettuSumma toimintaa siten, että muuttuja loppu määritellään funktion pankki sisällä paikalliseksi muuttujaksi.

function pankki() {
    var loppu = 2;
    
    var rajoitettuSumma = function(alku) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        return summa;
    };

    return rajoitettuSumma;
}

Koska JavaScriptissä on funktionäkyvyys, näkyy muuttuja loppu funktion pankki sisällä olevalle funktiolle. Funktiossa rajoitettuSumma voidaan muuttaa muuttujan loppu arvoa -- itseasiassa muuttuja loppu on olemassa useamman rajoitettuSumma-funktiokutsun ajan.

function pankki() {
    var loppu = 2;
    
    var rajoitettuSumma = function(alku) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        loppu++;
        return summa;
    };

    return rajoitettuSumma;
}
var funktio = pankki();
alert(funktio(1)); // tulostaa 1
alert(funktio(1)); // tulostaa 3
alert(funktio(1)); // tulostaa 6
alert(funktio(1)); // tulostaa 10

Voimme palauttaa pankista olion. Alla olevassa esimerkissä palautamme olion, jonka attribuutti alhaaltaRajoitettuSumma käyttää funktiota rajoitettuSumma.

function pankki() {
    var loppu = 2;
    
    var rajoitettuSumma = function(alku) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        loppu++;
        return summa;
    };

    return {
        alhaaltaRajoitettuSumma: rajoitettuSumma
    };
}

Nyt funktio pankki palauttaa olion, jolla on attribuutti alhaaltaRajoitettuSumma. Attribuuttiin pääsee käsiksi aivan kuten olioiden attribuutteihin normaalistikin.

var funktio = pankki();
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 1
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 3
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 6
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 10

// MUTTA!
alert(funktio.rajoitettuSumma(1)); // ei onnistu!

Funktioon rajoitettuSumma ei kuitenkaan pääse suoraan käsiksi!

Luodaan pankille vielä toinen funktio, joka asettaa muuttujan loppu arvon.

function pankki() {
    var loppu = 2;
    
    var rajoitettuSumma = function(alku) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        loppu++;
        return summa;
    };

    var asetaLoppu = function(uusiLoppu) {
        loppu = uusiLoppu;
    }

    return {
        alhaaltaRajoitettuSumma: rajoitettuSumma,
        asetaLoppu: asetaLoppu
    };
}
var funktio = pankki();
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 1

funktio.asetaLoppu(4);
alert(funktio.alhaaltaRajoitettuSumma(1)); // tulostaa 10

Yllä olevat funktiot voidaan kirjoittaa myös siten, että niille määritellään nimi osana funktiomäärittelyä. Tällöin erilliselle muuttujalle ei ole tarvetta.

function pankki() {
    var loppu = 2;
    
    function rajoitettuSumma(alku) {
        var summa = 0;
        for (var i = alku; i < loppu; i++) {
            summa += i;
        }
    
        loppu++;
        return summa;
    };

    function asetaLoppu(uusiLoppu) {
        loppu = uusiLoppu;
    }

    return {
        alhaaltaRajoitettuSumma: rajoitettuSumma,
        asetaLoppu: asetaLoppu
    };
}

Moduulit

Module Pattern hyödyntää sekä anonyymejä funktioita että sulkeumia sovelluksen toimintalogiikan kapselointiin. Ajatuksena on luoda ensin nimiavaruudessa käytettävä muuttuja, jonka kautta sovelluksen eri osia käytetään. Luodaan olio hallinta, johon lisätään kirjanpitotoiminnallisuus.

// huom! luodaan tyhjä olio, jolle voi lisätä attribuutteja
var hallinta = {};

hallinta.kirjanpito = (function() {
    var kurssisuoritukset = new Array();
    var opiskelijanSuoritukset = {};
    var kurssinSuoritukset = {};

    function lisaaSuoritus(kurssi, opiskelija, arvosana) {
        var suoritus = new Kurssisuoritus(opiskelija, kurssi, arvosana);
    
        // metodilla push lisätään listaan
        kurssisuoritukset.push(suoritus);

        // erilliset metodit opiskelijakohtaisten ja kurssikohtaisten suoritusten
        // lisäämiseen
        lisaaOpiskelijanSuoritus(opiskelija.nimi, suoritus);
        lisaaKurssinSuoritus(kurssi.nimi, suoritus);
    }

    function haeOpiskelijanKurssisuoritukset(opiskelijanNimi) {
        return opiskelijanSuoritukset[opiskelijanNimi];
    }

    function haeKurssinSuoritukset(kurssinNimi) {
        return kurssinSuoritukset[kurssinNimi];
    }

    function haeKaikkiSuoritukset() {
        return kurssisuoritukset;
    }

    // apufunktiot
    function lisaaOpiskelijanSuoritus(opiskelijanNimi, suoritus) {
        // jos opiskelijan nimellä ei ole yhtäkään suoritusta, on nimellä
        // saatava arvo false -- luodaan tällöin lista suorituksille
        if(!opiskelijanSuoritukset[opiskelijanNimi]) {
            opiskelijanSuoritukset[opiskelijanNimi] = new Array();
        }

        opiskelijanSuoritukset[opiskelijanNimi].push(suoritus);
    }

    function lisaaKurssinSuoritus(kurssinNimi, suoritus) {
        if(!kurssinSuoritukset[kurssinNimi]) {
            kurssinSuoritukset[kurssinNimi] = new Array();
        }

        kurssinSuoritukset[kurssinNimi].push(suoritus);
    }

    // julkaistava rajapinta
    return {
        lisaaSuoritus: lisaaSuoritus,
        haeOpiskelijanSuoritukset: haeOpiskelijanKurssisuoritukset,
        haeKurssinSuoritukset: haeKurssinSuoritukset, 
        haeKaikkiSuoritukset: haeKaikkiSuoritukset
    };
})();

Yllä oleva moduuli kapseloi kurssihallinnan siten, että hallinnan kapseloimiin tietueisiin ei pääse käsiksi. Voimme jatkossa käyttää kirjanpito-ohjelmaa seuraavasti:

var weso = new Kurssi("Web-selainohjelmointi");
var mikke = new Opiskelija("Michael");

// EI ONNISTU!
// hallinta.kirjanpito.kurssisuoritukset.push(new Kurssisuoritus(weso, mikke, 1));

// ONNISTUU!
hallinta.kirjanpito.lisaaSuoritus(weso, mikke, 1);

Module Pattern ja Oliot

Miten module pattern liittyy olioihin? Voiko module patternia käytettäessä samasta moduulista luoda useampia olioita?

 

 

Kapseloitu laskuri

Toteuta tehtäväpohjaan laskin aiemmin esitellyllä Module Patternilla. Laskimen tulee kapseloida muuttuja luku ja tarjoata funktiot kasvata, joka kasvattaa luvun arvoa yhdellä, ja annaLuku, joka palauttaa luvun. Luo laskin tehtäväpohjassa esiteltyyn muuttujaan var laskin;.

Huom! Laskimen sisäiseen rakenteeseen ei tule voida vaikuttaa muuten kuin funktioiden kasvata ja annaLuku kautta.

Palauta sovelluksesi TMC:lle kun se toimii toivotusti.

PersonManager

Tehtäväpohjaan on hahmoteltu henkilöiden hallintaan sopivan sovelluksen rakennetta. Tehtäväsi on jatkaa sitä eteenpäin siten, että henkilöiden lisääminen sovellukseen onnistuu.

Tehtävänäsi on toteuttaa:

  1. Lomakkeen napin käsittelytoiminnallisuus (manager.gui.buttonPressed())
  2. Henkilön lisääminen (manager.data.addPerson(person))
  3. Henkilöiden listaaminen (manager.data.list())

Sekä lisätä manager.data-moduuliin funktioiden sopiva näkyvyys. Tehtäväpohjassa on lisää ohjeita.

Huom! Älä poikkea jo hahmotellusta module pattern-suunnittelumallia seuraavasta rakenteesta. Ainoa manager-nimialueen ulkopuolella oleva funktiokutsu on sovellukseen jo määritelty init.

Kun sovelluksesi toimii palauta se TMC:lle.

Huom! Maanantaina julkaistussa tehtäväpohjassa oli virhe, joka korjattiin tiistaina klo 15:25. Jos hait tehtävän ennen päivitystä ja haluat käynnistää sen palvelimella, joudut muuttamaan WEB-INF -kansiossa olevan web.xml-tiedoston versioksi version 2.4. Tiedoston pitäisi siis olla muotoa:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    ...

Keskustelu palvelimen kanssa

Selaimessa toimivia ohjelmistoja rakennettaessa yhdeksi kysymykseksi tulee sovelluksen käyttämän datan säilöminen. Jotta sovelluksen sisältämä tieto olisi kaikkien käyttäjien saatavilla, tulee se tallentaa erilliselle palvelimelle, jonne kaikilla on pääsy. Palvelinohjelmistojen suunnittelu ja toteuttaminen on hyvin laaja alue, jota esimerkiksi viime periodissa järjestetty kurssi Web-palvelinohjelmointi raapaisee. Kurssin materiaali löytyy osoitteesta http://www.cs.helsinki.fi/group/java/s12-wepa/.

Käytännössä keskustelu palvelimen kanssa tapahtuu HTTP-protokollaa käyttäen. HTTP-protokolla on tekstimuotoinen protokolla, joka sisältää pyyntötyyppejä erilaisten pyyntöjen tekemiseen. Pyyntötyypillä GET haetaan dataa, POST lähetetään dataa palvelimelle, ja DELETE poistetaan dataa. Selaimet tarjoavat abstraktiokerroksen HTTP-protokollan päälle -- itseasiassa kun selaimessa haetaan web-sivua jostain osoitteesta, esimerkiksi osoitteesta http://www.cs.helsinki.fi/group/java/s12-wepa/, selain tekee HTTP-pyynnön osoitteessa www.cs.helsinki.fi -olevalle palvelimelle, ja pyytää sieltä polussa /group/java/s12-wepa/ olevaa resurssia.

Selainohjelmistoja rakennettaessa palvelimelta voidaan pyytää kokonaista HTML-dokumenttia, tai pienempää datamäärää. Nykyään uusissa sovelluksissa suosituin datan siirtoformaatti on JSON.

JSON

JSON (JavaScript Object Notation) on JavaScriptin käyttämä tiedonsiirtoformaatti, jonka suosio perustuu helppoon JavaScript-olioiksi muuttamiseen. Olemme aiemmin luoneet JavaScript-olioita seuraavanlaisella notaatiolla.

var mikke = {nimi: "Mikke", syntymavuosi: 1984};

JSON-formaatti on hyvin samannäköinen. JSON-muodossa data kuvataan merkkijonoina. Luodaan ylläolevaa oliota kuvaava JSON-merkkijono mikkeData. Huomaa kahden erilaisen hipsun käyttö!

var mikkeData = '{"nimi": "Mikke", "syntymavuosi": 1984}';

Nyt käytössämme on merkkijono, jonka sisältö näyttää lähes samalta kuin aiemmin olion luontiin käyttämämme notaatio. Miten tästä saa olion?

JavaScript tarjoaa toiminnallisuuden merkkijonon JavaScript-olioksi ja takaisin muuttamiseen. Funktio JSON.parse muuttaa parametrina annetun merkkijonon JavaScript-olioksi, ja funktio JSON.stringify muuttaa parametrina annetun JavaScript olion merkkijonoksi.

var mikkeData = '{"nimi": "Mikke", "syntymavuosi": 1984}';
var mikke = JSON.parse(mikkeData);
alert(mikke.nimi);

var mikkeKlooni = JSON.stringify(mikke);
alert(mikkeKlooni);

Helppoa kuin heinänteko.

Datan hakeminen palvelimelta

Datan noutamiseen palvelimelta käytetään muutamaa erillistä lähestymistapaa. Suurin osa selaimista tukee XMLHttpRequest-oliota, jonka avulla voidaan luoda pyyntöjä palvelimelle. Käytännössä pyynnön lähettäminen ja käsittely tapahtuu kolmessa vaiheessa. Ensin luodaan XMLHttpRequest-olio, sitten määritellään oliolle vastauksen käsittelevä funktio, ja lopuksi lähetetään pyyntö.

// pyyntöolion luonti
var req = new XMLHttpRequest();

// mitä tehdään kun saadaan vastaus (vastauksia voi olla useita)
req.onreadystatechange = function() {
    // jos tila ei ole 4 (valmis), ei käsitellä 
    if (req.readyState !== 4) {
        alert("state " + req.readyState);
        return false;
    }

    // jos statuskoodi ei ole 200 (ok), ei käsitellä
    if (req.status !== 200) {
        alert("status " + req.status);
        return false;
    }

    // näytetään vastaus
    alert(req.responseText);
}

req.open("GET", "data.json", true);
req.send("");

Tutkitaan yllä olevaa koodia hieman tarkemmin. Palvelin voi palauttaa XMLHttpRequest-pyyntöön useamman vastauksen. Attribuutti readyState sisältää arvon väliltä [0, 4], missä 4 tarkoittaa pyynnön olevan valmis. Jos attribuutin readyState arvo ei ole neljä, odotamme lopullista vastausta. Attribuutti status kertoo HTTP-pyynnön statuskoodin. Statuskoodi 200 kertoo pyynnön onnistuneen. Lisää tietoa statuskoodeista löytyy esimerkiksi googlella ja täältä.

Ylläolevassa esimerkiksi vastaus näytetään alert-komennon avulla. Käytännössä JSON-dataa sisältävän vastauksen voisi muuttaa JSON.parse-funktiolla olioksi.

Tärkeä osa liittyy pyynnön avaamiseen. Komento req.open("GET", "data.json", true); avaa GET-tyyppisen HTTP-yhteyden nykyiseen sivustoon liittyvään osoitteeseen data.json. Koska viimeinen parametri on true, on pyyntötyyppi asynkroninen, eikä selain jää odottamaan vastausta. Vastaukseen reagoidaan kun vastaus saapuu.

Kun kokeilet ylläolevaa koodia eri osoitteilla, huomaat että datan hakeminen ei aina onnistu. XMLHttpRequest-pyyntöihin liittyy tietoturvarajoitteita, jotka oletuksena rajoittavat pyynnön tekemisen samaan osoitteeseen.

Pyyntöjen tekeminen oman palvelimen ulkopuolelle

Pyyntöjen tekemistä eri osoitteisiin rajoittaa ns. "Same origin policy", jolla pyritään rajoittamaan muunmuassa pyynnön mukana lähetettävän datan (evästeet, kirjautumistiedot ym.) päätymistä vääriin käsiin. Sivustot, jotka koostavat useampia palveluita yhteen kuitenkin tarvitsevat pääsyn ulkopuoliseen dataan.

W3C työskentelee CORS (Cross-origin resource sharing)-spesifikaation kanssa parhaillaan. CORS-spesifikaation tavoitteena on määritellä tuki domain-riippumattomalle resurssien jakamiselle. Käytännössä tuki vaatii sen, että palvelinohjelmiston vastauksessa on otsakkeet, jotka kertovat osoitteet, joissa haettua dataa voi käyttää.

Rajoituksen kiertämiseen on kehitetty useita tekniikoita, mm. proxy-mekanismi, iframe-elementtien kanssa toimiminen, ja JSONP. Proxy-mekanismissa paikalliselle palvelimelle luodaan skripti, joka hakee kolmannen osapuolen datan paikalliselle palvelimelle, jolloin selaimen näkökulmasta data on paikallista. IFrame-elementtiä käytettäessä taas sivu haetaan erilliseen iframe-elementtiin, josta haetaan tarvitut osat.

Pyyntöjen tekeminen omalle koneelle

Osa selaimista kieltää pyyntöjen tekemisen suoraan tiedostojärjestelmään. Voit kiertää tämän esimerkiksi chromessa käynnistämällä chromen parametrilla "--disable-web-security".

JSONP

JSONP (JSON with padding) hyödyntää tietoa siitä, että script-elementin osoite, eli paikka josta JavaScript-lähdekoodi haetaan, ei ole rajoitettu. Data haetaan asettamalla JavaScriptillä luotavalle script-elementille src-attribuutti, johon asetetaan JSONP-muotoista dataa tarjoavan palvelimen osoite.

Dataa haettaessa pyynnölle annetaan parametrina funktion nimi, jonka nimisellä funktiolla data tulee kapseloida palvelinpäässä. Esimerkiksi, jos pyyntö tehdään osoitteeseen palvelin/data.jsonp?callback=handleResponse, on vastauksessa tulevan datan oltava muotoa handleResponse(data). Funktio handleResponse on määritelty osana dataa hakevaa sivua.

// aiemmin määritelty funktio handleResponse
function handleResponse(content) {
    alert(content);
}
// dataa haettaessa tehtävä kutsu. Haetaan tietyssä osoitteessa olevasta palvelusta
// jsonp-muotoista dataa.
var script = document.createElement("script");
script.setAttribute("src", "osoite/data.jsonp?callback=handleResponse");
document.body.appendChild(script);

Sivu voi palauttaa esimerkiksi seuraavanlaisen vastauksen vastauksen.

handleResponse({"nimi":"mikke", "ika":17})

TweetTweet

Twitter tarjoaa JSONP-apin, jonka avulla pääsemme käsiksi twitter-viesteihin. Tässä tehtävässä toteutat osan twitter-viestien lataamiseen ja näyttämiseen tarkoitetusta palvelusta. Tehtävänäsi on toteuttaa viestien palvelimelta lukemiseen liittyvä toiminnallisuus.

Toteuta tehtäväpohjassa olevaan moduuliin tweet.data funktiot load, parse, ja list. Funktiolle load annetaan parametrina osoite, josta viestit ladataan. Funktiota parse kutsutaan kun viestien lataaminen on valmis, ja funktio list palauttaa ladatut viestit. Kutsu funktion parse lopussa funktiota displayHook, jolloin haettu data näytetään käyttäjälle.

Käytä osoitetta http://api.twitter.com/1/statuses/user_timeline/rageresearch.json?count=4 testaamiseen. Saat callback-funktioksi funktion tweet.data.parse esimerkiksi seuraavalla osoitteella:

http://api.twitter.com/1/statuses/user_timeline/rageresearch.json?count=4&callback=tweet.data.parse

Yllä olevalla osoitteella sivun tulee näyttää seuraavalta.

Kun sovelluksesi toimii, ja näet twitter-viestit index.html-sivulla, palauta se TMC:lle.

Datan lähettäminen palvelimelle

Datan lähettämiseen liittyy samat haasteet kuin datan vastaanottamiseen. Yksinkertaisin tapa lähettää tietoa palvelimelle on XMLHttpRequest-olion GET-pyyntö siten, että lähetettävä data asetetaan mukaan pyynnön osoitteeseen. Käytännössä parametrina oleva data käsitellään palvelinpuolella pyyntöä kuuntelevassa ohjelmistossa.

// pyyntöolion luonti
var req = new XMLHttpRequest();
var parametrit = "nimi=mikke&ika=17";
req.open("GET", "dataprocessor.html?" + parametrit, true);
req.send("");

GET-pyyntö on hieman huono siinä mielessä, että lähetettävä data näkyy kaikkialle. Esimerkiksi jos pyyntö kulkee useamman reitittimen läpi ennen pääsyä palvelimelle, jokainen reititin näkee parametrit. Toinen vaihtoehto on POST-pyyntö, jossa data lähetetään osana pyynnön runkoa. Tällöin pyynnölle tulee myös määritellä lähetettävän datan muoto. Allaolevassa esimerkissä sanomme datan olevan lomakkeelta.

var req = new XMLHttpRequest();
var data = "nimi=mikke&ika=17";
req.open("POST", "dataprocessor.html", true);

req.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
req.setRequestHeader("Content-Length", data.length);
req.setRequestHeader("Connection", "close");

req.send(data);

JSON-muotoisen datan lähettäminen osana POST-pyyntöä onnistuu vastaavasti.

var mikke = {nimi: "Michael", ika: 17};
var data = JSON.stringify(mikke);

var req = new XMLHttpRequest();
req.open("POST", "jsonprocessor.html", true);

req.setRequestHeader("Content-Type","application/json");
req.setRequestHeader("Content-Length", data.length);
req.setRequestHeader("Connection", "close");

req.send(data);

Submission

Toteuta tehtäväpohjassa olevaan lähdekooditiedostoon submission.io-moduuli, jolla on kaikille näkyvä funktio send. Funktio send saa parametrina JavaScript-olion, joka tulee lähettää palvelimelle JSON-muodossa.

Lähetä data osoitteeseen http://bad.herokuapp.com/app/in. Voit tarkistaa menikö data perille osoitteessa http://bad.herokuapp.com/app/out. Ennenkuin aloitat, kannattaa vierailla sivulla http://bad.herokuapp.com/ niin varmistat että sovellus on päällä.

Kun sovelluksesi lähettää dataa palvelimelle, ja näet lähetetyn datan palvelimella, palauta tehtävä TMC:lle. Jos data menee palvelimelle, ja näet virheen "XMLHttpRequest cannot load http://bad.herokuapp.com/app/in. Origin null is not allowed by Access-Control-Allow-Origin." -- älä välitä siitä. Sovellus herokussa ei ole konfiguroitu täysin oikein.

Chat-chat (4p)

Tämä on avoin tehtävä, jonka tekemisestä saa 4 pistettä. Tehtävästä saa pisteet vain jos sovellus toimii kokonaisuudessaan.

Osoitteessa http://bad.herokuapp.com/app/ toimii chat-sovelluksen backend-toiminnallisuus. Tässä tehtävässä rakennetaan chatille selainpuolen toiminnallisuus.

Huom! Käytä tehtävässä vain yhtä HTML-dokumenttia. Tehtäväpohjassa on mukana tiedosto index.html, jonka sisään toiminnallisuutta voi toteuttaa. Toteuta chatin logiikka luonnollisesti erillisinä JavaScript-tiedostoina.

Sisäänkirjautuminen

Kun käyttäjä avaa chat-sivun, näytetään hänelle login-näkymä, joka näyttää seuraavalta.

Kun käyttäjä kirjoittaa käyttäjätunnuksen ja painaa Login-nappia, selainsovellus lähettää palvelimelle JSON-merkkijonon, joka on muotoa { "nickname": nick }, missä nick on käyttäjän kirjoittama käyttäjätunnus. Kirjautumispyyntö tehdään HTTP POST-pyyntönä osoitteeseen http://bad.herokuapp.com/app/auth. Jos kirjautuminen onnistuu, palvelin palauttaa statuskoodin 200, muuten statuskoodi on jokin muu. Ilman kirjautumista palvelimelle ei voi lähettää viestejä.

Viestien listaaminen

Kirjautumisen onnistuessa käyttäjälle näytetään chat-näkymä, joka näyttää tyhjänä seuraavalta:

Kirjautumisen yhteydessä palvelimelta tulee myös hakea lista viimeisimmistä viesteistä, jotka näytetään näkymässä. Viestit saa haettua HTTP GET-pyynnöllä osoitteesta http://bad.herokuapp.com/app/messages. Jos viestien hakeminen onnistuu, palvelin palauttaa statuskoodin 200, muuten statuskoodi on jokin muu.

Palvelin palauttaa viimeisimmät viestit JSON-taulukossa (array). Yksittäinen viesti sisältää seuraavat tiedot:

{
    "id": 4,
    "timestamp": 1352114153691,
    "nickname": "El Barto",
    "message": "Hello world!"
}

Viestin aikaleima kuvaa millisekunteja epoch-ajankohdasta (1.1.1970). Esimerkiksi JavaScriptin Date-oliot osaavat tulkita tällaista lukua ja palauttaa normaalin päivämäärän ja kellonajan sen perusteella. Palvelin palauttaa viestit siten, että uusin viesti on ensimmäinen.

Jos palvelimelle on jo lähetetty aiemmin viestejä, kirjautumisen jälkeen chat-näkymä näyttää esimerkiksi seuraavalta:

Viestin lähetys

Toteuta viestin lähettäminen chat-näkymään. Send-nappia painettaessa sovelluksen tulee lähettää tekstikentässä oleva viesti palvelimelle. Uusi viesti tulee lähettää HTTP POST-pyynnöllä osoitteeseen http://bad.herokuapp.com/app/messages. Pyynnössä lähetettävä viesti näyttää esimerkiksei seuraavalta:

{
    "nickname": "El Barto",
    "message": "Huh-huh!"
}

Hae viestin lähettämisen jälkeen palvelimelta uusimmat viestit ja päivitä chat-näkymä saaduilla viesteillä, jotta juuri lähetetty viesti näkyy sivulla.

Viestien päivittäminen ja uloskirjautuminen

Toteuta chat-näkymän Refresh-napin toiminnallisuus. Napin painalluksen tulee hakea palvelimelta uusimmat viestit ja näyttää ne chat-näkymässä.

Toteuta chat-näkymän Logout-napin toiminnallisuus. Napin painalluksen tulee piilottaa chat-näkymä ja palauttaa login-näkymä sivulle, jotta chattiin voi kirjautua uudella nimimerkillä. Nimimerkille varatun tekstikentän tulee olla tyhjä. Samoin uudelleen kirjauduttaessa sisään chat-näkymän viestille varatun tekstikentän tulee olla tyhjä.

Kun olet valmis, lähetä toteutuksesi TMC:lle. Tehtävän ratkaisuehdotus käyttää hieman erilaista lähestymistapaa, jossa module patternia käytetään olioiden luomiseen. Palaamme siihen seuraavalla viikolla.

Oliot ja Moduulit

Syvennytään lisää olioihin ja moduuleihin.

Oliot

Oliot ovat funktioiden ilmentymiä, jotka luodaan new-avainsanalla. Oliolla on oma oliokohtainen tila, mihin pääsee käsiksi this-operaattorilla. Esimerkiksi alla on luotu funktio Kirja, josta voi luoda olioita. Kirjalle on määritelty attribuutit nimi ja julkaisuvuosi.

function Kirja(nimi, julkaisuvuosi) {
    this.nimi = nimi;
    this.julkaisuvuosi = julkaisuvuosi;
}

// funktiosta luodaan olio new-operaattorilla
kalevala = new Kirja("Kalevala", 1835);

Olioille määritellään metodeja prototyyppiperinnän avulla. Prototyypin muokkauksen jälkeen olioilla on käytössä juuri määritellyt funktiot.

function Kirja(nimi, julkaisuvuosi) {
    this.nimi = nimi;
    this.julkaisuvuosi = julkaisuvuosi;
}

Kirja.prototype.tulostaNimi = function() {
    alert(this.nimi);
}

// funktiosta luodaan olio new-operaattorilla
kalevala = new Kirja("Kalevala", 1835);
kalevala.tulostaNimi(); // Kalevala

elefantinMatka = new Kirja("Elefantin matka", 2008);
elefantinMatka.tulostaNimi(); // Elefantin matka

Metodit määritellään käytännössä aina heti olion luovan funktion määrittelyn jälkeen. Käytännössä new Kirja("Kalevala", 1835)-kutsun suorituksessa uusi kalevala-olio peritään Kirja.prototype -prototyypistä. Tämän jälkeen sen konstruktori suoritetaan, ja sille allokoidaan olion attribuuttien tarvitsema tila. Lopulta konstruktori palauttaa viitteen uuteen olioon.

JavaScript ei mahdollista this-operaattorilla esiteltyjen attribuuttien kapselointia, vaan ne ovat julkisia.

function Kirja(nimi, julkaisuvuosi) {
    this.nimi = nimi;
    this.julkaisuvuosi = julkaisuvuosi;
}

Kirja.prototype.tulostaNimi = function() {
    alert(this.nimi);
}

// funktiosta luodaan olio new-operaattorilla
kalevala = new Kirja("Kalevala", 1835);

// olion attribuutteihin pääsee käsiksi suoraan
kalevala.nimi = "Valekala";

kalevala.tulostaNimi(); // Valekala

Konstruktorifunktiot nimetään isolla alkukirjaimella, olioiden nimet pienellä alkukirjaimella.

Puhelinmuistio

Toteuta funktio Puhelinmuistio, joka luo puhelinmuistio-olion. Puhelinmuistioon voi lisätä nimiä ja numeroita. Jokaiseen nimeen voi liittyä useampi numero. Numeroiden lisäämisen tulee tapahtua lisaaNumero-funktiolla, ja puhelinmuistion tulee tarjota metodi annaNumerot, jolle annetaan parametrina nimi.

Jos samalle henkilölle yritetään asettaa sama numero useampaan kertaan, numero tallennetaan henkilölle vain kerran. Useammalla henkilöllä voi olla sama numero.

Tehtäväpohjan mukana olevalle HTML-sivulle ei tarvitse tehdä mitään. Voit hyödyntää tehtäväpohjassa tulevaa Array-funktion laajennusta contains omassa toteutuksessasi. Kun tehtävä toimii seuraavilla esimerkeillä, palauta se TMC:lle.

muistio = new Puhelinmuistio();
muistio.lisaaNumero("mikke", "044-33669933");
muistio.lisaaNumero("mikke", "044-33669933");
alert(muistio.annaNumerot("mikke")); // numero 044-33669933 vain kerran

muistio.lisaaNumero("mikke", "231");
alert(muistio.annaNumerot("mikke")); // numerot 044-33669933 ja 231

alert(muistio.annaNumerot("matti")); // tyhjä lista
muistio.lisaaNumero("matti", "1111");
alert(muistio.annaNumerot("matti")); // numero 1111

alert(muistio.annaNumerot("mikke")); // numerot 044-33669933 ja 231

Moduulit

Moduulit toteutetaan anonyymin funktion avulla. Muuttujien funktionäkyvyyden takia muuttujat voidaan kapseloida anonyymin funktion sisään, jolloin niihin ei pääse käsiksi funktion ulkopuolelta. Anonyymin funktion sisälle määritellyt funktiot pääsevät käsiksi muuttujiin, jolloin sisäfunktioissa voidaan muokata muuttujien arvoja. Moduuli palauttaa moduulissa määritellyn rajapinnan, jossa on viittaukset sisäfunktioihin.

Hahmotellaan kaupan hallinnointiin tarvittavaa järjestelmää. Luodaan ostoskorimoduuli, joka tarjoaa julkisen rajapinnan tuotteiden lisäämiseen ja tuotteiden lukumäärän laskemiseen.

var kauppa = {};

kauppa.ostoskori = (function() {
    var ostokset = [];

    function lisaaOstos(tuotteenNimi) {
        if(!ostokset[tuotteenNimi]) {
            // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
            ostokset[tuotteenNimi] = 0;
        }

        // kasvatetaan tuotteen lukumäärää yhdellä
        ostokset[tuotteenNimi]++;
    }

    function tuotteitaYhteensa() {
        var lukumaara = 0;
        for(var tuotteenNimi in ostokset) {
            lukumaara += ostokset[tuotteenNimi];
        }
        
        return lukumaara;
    }

    // rajapinta
    return {
        lisaa: lisaaOstos,
        tuotteidenLukumaara: tuotteitaYhteensa
    };
})();

Ostoskoria voi käyttää nyt seuraavasti:

kauppa.ostoskori.lisaa("keksi");
kauppa.ostoskori.lisaa("keksi");
kauppa.ostoskori.lisaa("omena");
alert(kauppa.ostoskori.tuotteidenLukumaara()); // 3

Anonyymille funktiolle voi antaa parametreja. Luodaan hinnastomoduuli, joka palauttaa tuotteen nimen perusteella sen hinnan. Vaikka hinnastomoduulimme palauttaa kaikkien tuotteiden hinnaksi 3, voisi sen toteutus myös hakea hinnat esimerkiksi erilliseltä palvelimelta.

kauppa.hinnasto = (function() {
    function annaHinta(tuote) {
        return 3;
    }

    return {
        hinta: annaHinta
    };
})();

Laajennetaan ostoskorimoduulia siten, että se saa hinnaston parametrina. Lisätään ostoskorille myös funktio ostoskorissa olevien tuotteiden hinnan laskemiseen.

kauppa.ostoskori = (function(hinnasto) {
    var ostokset = [];

    function lisaaOstos(tuotteenNimi) {
        if(!ostokset[tuotteenNimi]) {
            // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
            ostokset[tuotteenNimi] = 0;
        }

        // kasvatetaan tuotteen lukumäärää yhdellä
        ostokset[tuotteenNimi]++;
    }

    function tuotteitaYhteensa() {
        var lukumaara = 0;
        for(var tuotteenNimi in ostokset) {
            lukumaara += ostokset[tuotteenNimi];
        }
        
        return lukumaara;
    }

    function yhteishinta() {
        var summa = 0;
        for(var tuotteenNimi in ostokset) {
            summa += ostokset[tuotteenNimi] * hinnasto.hinta(tuotteenNimi);
        }
        
        return summa;
    }

    // rajapinta
    return {
        lisaa: lisaaOstos,
        tuotteidenLukumaara: tuotteitaYhteensa,
        yhteishinta: yhteishinta
    };
})(kauppa.hinnasto);

Huomaa miten riippuvuus hinnastoon nimetään moduulin sisällä uudestaan anonyymin funktion parametrien kautta. Moduulin sisällä hinnastoon viitataan muuttujalla hinnasto.

Ostoskorin tilaaminen ja varasto

Jatkokehitetään yllä olevaa esimerkkiä. Tehtävänäsi on luoda varastokirjanpitoa varten moduuli kauppa.varasto, joka tarjoaa seuraavat funktiot:

  1. lisaa(tuote, lukumaara) lisää annetun lukumärään tuotteita varastoon.
  2. ota(tuote, lukumaara) ottaa varastosta tuotteita halutun lukumäärän.
  3. saldo(tuote) palauttaa tuotteen varastosaldon.

Varastosaldo voi olla myös negatiivinen.

Lisää lisäksi ostoskorille funktio tilaa, jonka avulla käyttäjä voi tilata ostoskorista olevat tuotteet. Kun ostoskori tilataan, varastosta otetaan tuotteita ostoskorissa oleva määrä. Tyhjennä tilauksen lopuksi myös ostoskori.

Kytke varasto ostoskoriin siten, että ostoskori tietää varastosta. Kun sovelluksesi toimii seuraavalla koodilla, palauta se TMC:lle.

kauppa.ostoskori.lisaa("kivi");
kauppa.ostoskori.lisaa("kivi");
kauppa.ostoskori.lisaa("kivi");
alert(kauppa.ostoskori.tuotteidenLukumaara()); // 3
alert(kauppa.varasto.saldo("kivi")); // 0

kauppa.ostoskori.tilaa();
alert(kauppa.ostoskori.tuotteidenLukumaara()); // 0
alert(kauppa.varasto.saldo("kivi")); // -3

alert(kauppa.varasto.saldo("paperi")); // 0

kauppa.ostoskori.lisaa("kivi");
kauppa.ostoskori.lisaa("kivi");
kauppa.ostoskori.lisaa("paperi");

kauppa.ostoskori.tilaa();
alert(kauppa.varasto.saldo("kivi")); // -5
alert(kauppa.varasto.saldo("paperi")); // -1

kauppa.varasto.lisaa("kivi", 7);
alert(kauppa.varasto.saldo("kivi")); // 2

Tilaamisen tulee vain vähentää tavarat varastosta ja tyhjentää ostoskori, muuta toiminnallisuutta ei vielä tarvitse.

Moduulista ei voi tehdä olioita, joten ostoskoreja voi olla vain yksi. Tämä ei kuitenkaan ole toivottavaa.

Olioiden tila ja new

Operaatiota new kutsuttaessa funktiosta luodaan kopio, jolloin käytännössä varataan tilaa oliolle ja sen this-operaattorilla merkatuille muuttujille. Uusi, juuri luotava olio, on käytännössä joukko avain-arvo -pareja, jossa arvo voi olla funktio, muuttuja, tai olio. Koska muuttujat voivat olla funktioita, voi this-operaattorilla viitata funktioon.

Luodaan funktio Laskuri, jonka sisällä on muuttuja luku. Muuttujaa luku ei määritellä this-operaatiolla, vaan se on funktion sisälle kapseloitu.

function Laskuri() {
    var luku = 0;
}

Ylläolevaa funktiota voi kutsua sekä new-operaation avulla tai ilman. Jos funktiota kutsutaan ilman new- kutsua, kutsu on normaali funktiokutsuna. Toisaalta, jos funkitiota kutsutaan new-operaation kanssa, funktiosta luodaan uusi olio.

Laskuri(); // suorittaa funktion sisällä olevan koodin

var olio = new Laskuri(); // suorittaa funktion sisällä olevan koodin, luo olion, ja palauttaa sen erilliseen muuttujaan

Yllä olio on kopio Laskuri-funktion sisäisestä tilasta. Tilaan ei kuitenkaan pääse mitenkään käsiksi.

Lisätään funktioon Laskuri kaksi this-operaatiolla määriteltyä funktiota. Koska operaatiolla this määritellyt muuttujat ovat oliokohtaisia, ovat myös funktiot oliokohtaisia.

function Laskuri() {
    var luku = 0;
 
    this.kasvata = function() {
        luku++;
    }

    this.tulosta = function() {
        alert(luku);
    }
}

Tutkitaan nyt mitä tapahtuu kun kutsumme Laskuri-funktiota sekä ilman new-operaatiota, että new-operaation kanssa. Kutsutaan funktiota ensin ilman new-operaatiota.

Laskuri(); // yrittää suorittaa funktion sisällä olevan koodin, ei toimi

Kun funktiota Laskuri kutsutaan ilman new-operaatiota, näemme virheen "Cannot set property 'kasvata' of undefined". Tämä johtuu siitä, että this-operaatio liittyy aina olioon. Käytännössä yritämme lisätä oliolle uutta muuttujaa kasvata. Tämä epäonnistuu, sillä oliota, mille muuttujaa yritetään asettaa ei ole.

Kutsutaan seuraavaksi funktiota Laskuri new-operaation kanssa, eli luodaan siitä olio.

var laskin = new Laskuri();

Yllä olevassa kutsussa luodaan klooni funktion Laskuri sisällöstä, ja palautetaan viite klooniin. Klooni kapseloi muuttujan luku, mutta siihen pääsee käsiksi oliomuuttujien kasvata ja tulosta kautta. Voimme luoda yllä olevasta funktiosta useamman kopion.

var laskin = new Laskuri();
laskin.kasvata();
laskin.tulosta(); // 1

var toinen = new Laskuri();
toinen.tulosta(); // 0
laskin.tulosta(); // 1

laskin.kasvata();
laskin.tulosta(); // 2
toinen.tulosta(); // 0

Palataan ihan alkuun. Javascriptiin tutustuessa huomasimme, että uusia JavaScript-olioita voi luoda aaltosulkujen avulla. Oikeastaan, operaatio this-lisää oliolle uusia muuttujia aivan kuten aaltosulkunotaatiolla luotavalle oliolle lisätään uusia muuttujia. Yllä olevan laskurin voi toteuttaa myös seuraavasti:

function Laskuri() {
    var luku = 0;
 
    return {
        kasvata: function() {
            luku++;
        },
        tulosta: function() {
            alert(luku);
        }
    };
}

Ja seuraavasti:

function Laskuri() {
    var luku = 0;

    function kasvata() {
        luku++;
    }
 
    function tulosta() {
        alert(luku);
    }

    return {
        kasvata: kasvata,
        tulosta: tulosta
    };
}

Oleellista on se, että Kutsu new Laskuri() palauttaa uuden olion. Uudella oliolla on funktion Laskuri kapseloima muuttuja luku, sekä luodun olion tarjoamat julkiset funktiot kasvata ja tulosta. Huomaa että kutsu {} luo uuden Object-tyyppisen olion -- yllä olevan olion tyyppi ei siis ole Laskin!

Monta tapaa sanoa sama asia

JavaScriptiä on monimuotoisuutensa takia helposti hyvin vaikea ymmärtää. Yhden asian voi helposti sanoa viidelläkin eri tavalla, joka voi johtaa väärinkäsityksiin tai väärinymmärryksiin. Oleellista on kuitenkin ymmärtää että oliot ovat avain-arvo -pareja, joilla jokaisella voi olla erilaiset sisällöt. Arvot voivat olla funktioita, jotka muokkaavat toisia arvoja.

Funktionäkyvyys mahdollistaa tiedon kapseloinnin. Sulkeumien takia sisäfunktioilla on pääsy niiden kapseloivan funktion sisältämiin muuttujiin, myös silloin kun kapseloivan funktion suoritus on ohi.

Tavara ja Matkalaukku

Muokataan viime viikolla ollutta tehtävää siten, että käytetään edellä esitettyä olioiden esitystapaa. Muokkaa tehtäväpohjassa olevia konstruktorifunktiota Tavara ja Matkalaukku siten, että konstruktorifunktiot sisältävät luotaviin olioihin liitettävät metodit. Muokkaa ohjelmaa siten, että se toimii alla olevalla esimerkillä.

Kun ohjelmasi toimii kuten toivottu, lähetä se TMC:lle. Huom! Viimeiset 2 riviä saavat rikkoa ohjelman. Älä (vieläkään) aseta matkalaukkuun omaa paino-muuttujaa, vaan laske matkalaukun paino tavaroiden painosta.

var kivi = new Tavara("kivi", 3);
var kirja = new Tavara("kirja", 7);
var pumpuli = new Tavara("pumpuli", 0.001);

var laukku = new Matkalaukku(10);
var vuitton = new Matkalaukku(3);

laukku.lisaa(kivi);
alert("laukun paino, pitäisi olla 3: " + laukku.paino());
laukku.lisaa(kivi); // virhe: "Tavara lisätty jo, ei onnistu!"

laukku.lisaa(kirja);
alert("laukun paino, pitäisi olla 10: " + laukku.paino());

laukku.lisaa(pumpuli); // virhe: "Liian painava, ei pysty!"

alert("laukun paino, pitäisi olla 10: " + laukku.paino());

vuitton.lisaa(pumpuli);
alert("vuittonin paino, pitäisi olla 0.001: " + vuitton.paino());

// seuraavien komentojen ei pitäisi ainakaan muuttaa vuittonin painoa
pumpuli.paino = 300; // jos tavaralla on metodi paino, hajottaa ohjelman seuraavassa, muuten ei
alert("vuittonin paino, pitäisi olla vieläkin 0.001: " + vuitton.paino()); // paino ei ole muuttunut

Moduulien ja olioiden yhdistäminen

Suurimmat syyt moduulien käyttöön ovat käytettävien globaalien muuttujanimien vähentäminen sekä tiedon kapselointi. Aiemmin käyttämämme moduulit voidaan nähdä singleton-suunnittelumallia seuraavina olioina tai staattisina funktioina, jotka muokkaavat staattista tilaa. Moduuleista ei ole voinut luoda ilmentymiä.

Pohditaan aiempaa kauppakassaesimerkkiä, jossa olimme varanneet kaupan toiminnallisuutta varten muuttujan kauppa. Aiempi toteutuksemme ostoskorista oli moduuli, joka tarkoitti sitä, että ostoskoreja voi olla vain yksi kerrallaan. Alustava toteutus näytti seuraavalta:

var kauppa = {};

kauppa.ostoskori = (function() {
    var ostokset = [];

    function lisaaOstos(tuotteenNimi) {
        if(!ostokset[tuotteenNimi]) {
            // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
            ostokset[tuotteenNimi] = 0;
        }

        // kasvatetaan tuotteen lukumäärää yhdellä
        ostokset[tuotteenNimi]++;
    }

    function tuotteitaYhteensa() {
        var lukumaara = 0;
        for(var tuotteenNimi in ostokset) {
            lukumaara += ostokset[tuotteenNimi];
        }
        
        return lukumaara;
    }

    // rajapinta
    return {
        lisaa: lisaaOstos,
        tuotteidenLukumaara: tuotteitaYhteensa
    };
})();

Muutetaan ylläoleva moduuli funktioksi siten, että ostoskorista voi tehdä uusia olioita. Muokataan funktioita lisaaOstos ja tuotteitaYhteensa myös siten, että niiden nimet vastaavat yllä määriteltyä rajapintaa.

var kauppa = {};

kauppa.Ostoskori = function() {
    var ostokset = [];

    this.lisaa = function(tuotteenNimi) {
        if(!ostokset[tuotteenNimi]) {
            // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
            ostokset[tuotteenNimi] = 0;
        }

        // kasvatetaan tuotteen lukumäärää yhdellä
        ostokset[tuotteenNimi]++;
    }

    this.tuotteidenLukumaara = function() {
        var lukumaara = 0;
        for(var tuotteenNimi in ostokset) {
            lukumaara += ostokset[tuotteenNimi];
        }
        
        return lukumaara;
    }
}

Voimme nyt luoda uusia ostoskoreja new-operaatiolla.

var a = new kauppa.Ostoskori();
a.lisaa("kekseja");
a.lisaa("kekseja");
alert(a.tuotteita()); // 2

var b = new kauppa.Ostoskori();
alert(b.tuotteita()); // 0
alert(a.tuotteita()); // 2

Aiemmassa esimerkissämme ostoskorilla oli tiedossa hinnasto, jota ei ylläolevassa esimerkissä ole. Hinnaston lisääminen jokaisen ostoskorin konstruktorikutsun yhteydessä ei ole miellyttävää, joten muokataan edellisestä ostoskorista moduuli, joka kapseloi hinnaston. Haluamme myös säilyttää mahdollisuuden useamman ostoskorin luomiseen. Muokataan ensin ostoskoritoteutusta siten, että se on kapseloitu moduulin sisälle. Luodaan moduuli Ostoskori, joka funktiokutsun yhteydessä palauttaa moduulin kapseloiman konstruktorifunktion nimeltä Kori.

var kauppa = {};

kauppa.Ostoskori = (function() {

    // konstruktori
    function Kori() {
        // oliokohtaiset muuttujat
        var ostokset = [];

        // oliokohtaiset metodit
        this.lisaa = function(tuotteenNimi) {
            if(!ostokset[tuotteenNimi]) {
                // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
                ostokset[tuotteenNimi] = 0;
            }

            // kasvatetaan tuotteen lukumäärää yhdellä
            ostokset[tuotteenNimi]++;
        }

        this.tuotteidenLukumaara = function() {
            var lukumaara = 0;
            for(var tuotteenNimi in ostokset) {
                lukumaara += ostokset[tuotteenNimi];
            }
        
            return lukumaara;
        }
    }
    
    return Kori;
})();

Ylläolevassa koodissa määritellään anonyymin funktion sisällä konstruktorifunktio Kori, joka kapseloi ostoskorin toiminnallisuuden. Anonyymi funktio suoritetaan heti, sillä sen lopussa on sulut. Käytännössä funktio palauttaa konstruktorifunktion, joka asetetaan olion kauppa muuttujaan Ostoskori. Aiemmin tekemämme ohjelma toimii vieläkin.

var kori = new kauppa.Ostoskori();
kori.lisaa("kekseja");
kori.lisaa("kekseja");
alert(kori.tuotteidenLukumaara()); // 2

var laukku = new kauppa.Ostoskori();
alert(laukku.tuotteidenLukumaara()); // 0
alert(laukku.tuotteidenLukumaara()); // 2

Lisätään ostoskorille hinnasto. Käytämme hinnaston toteutuksena aiemmin luomaamme seuraavanlaista hinnastoa.

kauppa.hinnasto = (function() {
    function annaHinta(tuote) {
        return 3;
    }

    return {
        hinta: annaHinta
    };
})();

Hinnaston lisääminen onnistuu antamalla se parametriksi anonyymille funktiolle.

kauppa.Ostoskori = (function(hinnasto) {
    // moduulin sisäinen muuttuja, joka näkyy kaikille moduulin sisällä
    var hinnat = hinnasto;

    // konstruktori
    function Kori() {
        // oliokohtaiset muuttujat
        var ostokset = [];

        // oliokohtaiset metodit
        this.lisaa = function(tuotteenNimi) {
            if(!ostokset[tuotteenNimi]) {
                // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
                ostokset[tuotteenNimi] = 0;
            }

            // kasvatetaan tuotteen lukumäärää yhdellä
            ostokset[tuotteenNimi]++;
        }

        this.tuotteidenLukumaara = function() {
            var lukumaara = 0;
            for(var tuotteenNimi in ostokset) {
                lukumaara += ostokset[tuotteenNimi];
            }
        
            return lukumaara;
        }
    }
    
    return Kori;
})(kauppa.hinnasto);

Nyt ostoskorilla on käytössä hinnasto. Lisätään ostoskorille vielä metodi ostoskorissa olevien tuotteiden hinnan laskemiseen.

kauppa.Ostoskori = (function(hinnasto) {
    // moduulin sisäinen muuttuja, joka näkyy kaikille moduulin sisällä
    var hinnat = hinnasto;

    // konstruktori
    function Kori() {
        // oliokohtaiset muuttujat
        var ostokset = [];

        // oliokohtaiset metodit
        this.lisaa = function(tuotteenNimi) {
            if(!ostokset[tuotteenNimi]) {
                // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
                ostokset[tuotteenNimi] = 0;
            }

            // kasvatetaan tuotteen lukumäärää yhdellä
            ostokset[tuotteenNimi]++;
        }

        this.tuotteidenLukumaara = function() {
            var lukumaara = 0;
            for(var tuotteenNimi in ostokset) {
                lukumaara += ostokset[tuotteenNimi];
            }
        
            return lukumaara;
        }

        this.yhteishinta = function() {
            var summa = 0;
            for(var tuotteenNimi in ostokset) {
                summa += ostokset[tuotteenNimi] * hinnat.hinta(tuotteenNimi);
            }
        
            return summa;
        }
    }
    
    return Kori;
})(kauppa.hinnasto);
var kori = new kauppa.Ostoskori();
kori.lisaa("kekseja");
kori.lisaa("kekseja");
alert(kori.tuotteidenLukumaara()); // 2
alert(kori.yhteishinta()); // 6

var laukku = new kauppa.Ostoskori();
alert(laukku.tuotteidenLukumaara()); // 0
alert(laukku.yhteishinta()); // 0

alert(kori.tuotteidenLukumaara()); // 2
alert(kori.yhteishinta()); // 6

Ylläolevassa esimerkissä käytetään moduulille parametrina annettua hinnastoa hintojen laskemiseen. Itseasiassa, koska hinnasto on moduulin parametrina, on se käytössä myös moduulin sisällä. Moduuli ei siis tarvitse erillistä hinnat-muuttujaa.

kauppa.Ostoskori = (function(hinnasto) {

    // konstruktori
    function Kori() {
        // oliokohtaiset muuttujat
        var ostokset = [];

        // oliokohtaiset metodit
        this.lisaa = function(tuotteenNimi) {
            if(!ostokset[tuotteenNimi]) {
                // jos tuotetta ei ole lisätty ostoskoriin, lisätään se sinne
                ostokset[tuotteenNimi] = 0;
            }

            // kasvatetaan tuotteen lukumäärää yhdellä
            ostokset[tuotteenNimi]++;
        }

        this.tuotteidenLukumaara = function() {
            var lukumaara = 0;
            for(var tuotteenNimi in ostokset) {
                lukumaara += ostokset[tuotteenNimi];
            }
        
            return lukumaara;
        }

        this.yhteishinta = function() {
            var summa = 0;
            for(var tuotteenNimi in ostokset) {
                summa += ostokset[tuotteenNimi] * hinnasto.hinta(tuotteenNimi);
            }
        
            return summa;
        }
    }
    
    return Kori;
})(kauppa.hinnasto);
var kori = new kauppa.Ostoskori();
kori.lisaa("kekseja");
kori.lisaa("kekseja");
alert(kori.tuotteidenLukumaara()); // 2
alert(kori.yhteishinta()); // 6

var laukku = new kauppa.Ostoskori();
alert(laukku.tuotteidenLukumaara()); // 0
alert(laukku.yhteishinta()); // 0

alert(kori.tuotteidenLukumaara()); // 2
alert(kori.yhteishinta()); // 6

MV* ja Web-sovelluksen rakenne

Termi MVC (Model, View, Controller) esiintyy lähes kaikkialla ohjelmistotekniikassa. MVC on suunnittelumalli, joka pilkkoo sovelluksen kolmeen osaan: dataan (model), näkymään (view), ja käyttäjän interaktioita hallinnoivaan sovelluslogiikkaan eli kontrolleriin (controller). MVC-mallia on käytetty alunperin työpöytäsovelluksissa, mutta se on otettu käyttöön myös palvelin- ja selainpuolen ohjelmistoihin niiden kehittyessä.

Perusideat ovat säilyneet samoina. Käyttäjän tehdessä jotain, esimerkiksi painaessa sivulla olevaa nappia, toimintoon liittyvä tieto välittyy kontrollerille, joka päättää mitä seuraavaksi tehdään. Yleisin toiminto on mallin muokkaaminen tai korvaaminen palvelimelta haetulla datalla, ja uuden näkymän näyttäminen muokattuun malliin perustuen. Palvelinohjelmistoja rakennettaessa tämä tapahtuu esimerkiksi lähettämällä web-sivulla olevan lomakkeen data tiettyyn osoitteeseen, jossa kontrolleri odottaa pyyntöä. Kontrollerin vastaanottaessa pyynnön, pyyntöön liittyvä mahdollinen data tallennetaan. Tämän jälkeen luodaan uusi model, johon haetaan tietoa esimerkiksi tietokantapalvelusta. Model ohjataan näkymän luovalle komponentille, joka lopulta palauttaa uuden näkymän käyttäjälle.

Dynaamista toiminnallisuutta sisältävissä selainohjelmistoissa erityisesti vastaukset voivat sisältää paljon vähemmän dataa. Koko näkymää ei tarvitse hakea uudestaan jokaisen kyselyn yhteydessä.

Esimerkki: Muistuttaja

Luodaan sovellus, johon käyttäjä voi lisätä päiväkohtaisia tapahtumia. Jokaiseen tapahtumaan liittyy nimi ja aika. Luodaan aluksi sovellukselle nimiavaruus muistutus, johon sovelluksen toiminnallisuus lisätään.

Vaikka sovelluksessa käydään läpi sovelluksen osat termeillä Model, View, Controller, ei sovelluksen arkkitehtuuri seuraa MVC-mallia sen perinteisessä mielessä.

var muistutus = {};

View

MVC-mallissa näkymä vastaanottaa dataa, ja päättää miten se näytetään. Näkymä voi käyttää olemassaolevaa HTML-dokumenttia, ja asettaa siihen dataa, tai se voi luoda uusia elementtejä DOMin avulla. Huomaa että näkymä ja data on erotettu toisistaan, eli näkymä ei tiedä -- eikä välitä -- mallista. Se käsittelee vain dataa, jota sille annetaan. Luodaan HTML-dokumentti, jossa on paikka tapahtumille ja kentät uuden tapahtuman lisäämiselle.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>What's Happening?!</title>
    </head>
    <body onload="muistutus.init();" >

        <section id="tapahtumat">
	</section>
	
        <section id="uusitapahtuma">
            <label>Nimi: <input type="text" id="nimi" /></label>
            <label>Aika (vvvv-kk-pp): <input type="text" id="aika" /></label>
            <input type="button" id="lisaa" />
	</section>


        <script src="muistutus.js"></script>
    </body>
</html>

Luodaan näkymää varten oma nimiavaruus muistutus.view, ja lähdetään rakentamaan näkymän generointiin tarvittavaa toiminnallisuutta.

muistutus.view = {};

Luodaan tapahtumien listaamiseen tarvittava näkymä. Huomaa, että näkymä voi tarkoittaa myös sivun sisällä olevaa elementtiä. Luodaan listaus siten, että sille annetaan parametrina elementti, johon tapahtumia lisätään. Tapahtumien listaaminen tapahtuu metodissa listaaTapahtumat. Metodin listaaTapahtumat lisäksi näkymällä on metodi paivita, jota kutsutaan kun näkymä halutaan päivittää. Päivitä-metodille annetaan parametrina data, joka halutaan näyttää näkymässä.

muistutus.view.Listaus = function(elementti) {

    // julkiset metodit
    this.listaaTapahtumat = function(tapahtumat) {
        tyhjenna();

        for (var i = 0; i < tapahtumat.length; i++) {
            lisaaTapahtuma(tapahtumat[i]);
        }        
    }

    this.paivita = function(tapahtumat) {
        // päivitysoperaatio kutsuu listausoperaatiota
        this.listaaTapahtumat(tapahtumat);
    }

    // kapseloidut apufunktiot
    function lisaaTapahtuma(tapahtuma) {
        var tapahtumaElementti = document.createElement("h2");
        var teksti = tapahtuma.nimi + ' (' + tapahtuma.aika + ')';
        
        tapahtumaElementti.appendChild(document.createTextNode(teksti));
        elementti.appendChild(tapahtumaElementti);
    }

    function tyhjenna() {
        while(elementti.firstChild) {
            elementti.removeChild(elementti.firstChild);
        }
    }
}

Huomaa, että ylläoleva näkymässä aiheutuviin tapahtumiin liittyvää koodia ei sisällytetä näkymän koodiin. Jätetään ne kontrollerille. Koska elementti, johon tapahtumat lisätään, annetaan konstruktorifunktiolle parametrina, on se käytössä myös olion metodeissa.

Model

Tapahtumakalenteriin liittyvän mallin luominen on helpohkoa. Luodaan ensin oma nimiavaruus muistutus.domain.

muistutus.domain = {};

Luodaan nimiavaruuteen muistutus.domain konstruktori Tapahtumalista, joka tarjoaa toiminnallisuuden tapahtumien lisäämiseen ja kapselointiin. Tapahtumalista tietää jostain-näkymästä, jonka päivitysoperaatiota se kutsuu kun tapahtumia lisätään.

muistutus.domain.Tapahtumalista = function(view) {
    var tapahtumat = [];

    this.lisaaTapahtuma = function(tapahtuma) {
        tapahtumat.push(tapahtuma);

        view.paivita(tapahtumat);
    }

    this.annaTapahtumat = function() {
        return tapahtumat;
    }
}

Vaikka haluaisimme myös tehdä erillisen konstruktorin tapahtumalle, käytetään tapahtumia varten JavaScriptin omia olioita. Näin olioiden tallentamistoiminnallisuuden mahdollinen toteutus on helpompaa, sillä funktiota JSON.parse voi käyttää suoraan tapahtumat-muuttujaan.

Controller

Luodaan seuraavaksi kontrolleri. Kontrollerin tehtävänä on reagoida käyttöliittymässä tapahtuviin tapahtumiin, sekä toimia niiden pohjalta jotenkin. Luomme kontrollereille ensin oman nimiavaruuden muistutus.controller.

muistutus.controller = {};

Kontrolleri LomakeKontrolli tarjoaa rajapinnan lomake-elementtien käsittelyyn. Kontrollerille voi lisätä elementtejä, joita se kuuntelee. Se tarjoaa myös metodin lisaaTapahtuma, jota voi kutsua tapahtumankäsittelyn yhteydessä. Metodi lisaaTapahtuma käy läpi rekisteröidyt elementit, ja luo niiden pohjalta olion. Olio lähetetään lopulta mallille.

muistutus.controller.LomakeKontrolli = function(model) {
    var elementit = {};

    this.lisaaDataelementti = function(nimi, elementti) {
        elementit[nimi] = elementti;
    }

    this.lisaaTapahtuma = function(eventInformation) {
        var data = haeData();

        model.lisaaTapahtuma(data);

        tyhjennaElementit();
    }

    function haeData() {
        var data = {};
        for (var nimi in elementit) {
            data[nimi] = elementit[nimi].value;
        }

        return data;
    }
 
    function tyhjennaElementit() {
        for (var nimi in elementit) {
            elementit[nimi].value = "";
        }
    }
}

Kontrolleri sisältää toiminnallisuuden kontrolloitavien elementtien lisäämiseen, sekä elementtien sisältämän datan lähettämiseen tapahtumalistalle. Huomaa, että kontrolleri ei oikeastaan tiedä tapahtumien muodosta. Se vain kontrolloi lomakkeen elementtejä.

Sovelluksen alustaminen

Luodaan lopuksi alustusfunktio, joka luo sovelluksessa käytetyt oliot, sekä kytkee HTML-dokumentin elementit kontrolleriin.

muistutus.init = function() {
    // luodaan palaset
    var listausnakyma = new muistutus.view.Listaus(document.getElementById("tapahtumat"));

    var lista = new muistutus.domain.Tapahtumalista(listausnakyma);
    listausnakyma.listaaTapahtumat(lista.annaTapahtumat());

    var kontrolli = new muistutus.controller.LomakeKontrolli(lista);

    // kytketään kontrolli elementteihin
    kontrolli.lisaaDataelementti("nimi", document.getElementById("nimi"));
    kontrolli.lisaaDataelementti("aika", document.getElementById("aika"));
    
    document.getElementById("lisaa").addEventListener("click", kontrolli.lisaaTapahtuma, false);
}

Toimii! Sovellusta voisi esimerkiksi jatkokehittää siten, että se sisältäisi datan lähettämisen erilliselle palvelinkomponentille. Tämän lisäksi tapahtumia tulisi pystyä poistamaan.

Validointi

Tehtäväpohjan mukana tulee edellä käsitelty muistutussovellus. Jatkokehitä sovellusta siten, että sovelluksessa on validointitoiminnallisuus. Kun käyttäjä yrittää lisätä tapahtumaa, tulee tapahtuman tiedot validoida.

Toteuta validointitoiminnallisuus siten, että kontrolleriin voi lisätä validoijia. Kun käyttäjä lisää tapahtumaa, kaikki validoijat käydään läpi yksitellen siten, että data annetaan kullekin validoijalle vuorollaan. Jos validoijan palauttama viesti ei ole tyhjä, eli validoijalla on jotain valitettavaa, viesti tulostetaan alert-komennolla ja validointi lopetetaan. Tällöin tapahtuman lisääminen keskeytetään. Toteuta validoijat tehtäväpohjassa annetun Validoija-konstruktorin pohjalta siten, että kukin validoija on olio, jolla on validoitavan kentän nimen lisäksi validointifunktio, jota kontrollerin tulee kutsua dataa validoitaessa. Validoijan sisältämälle funktiolle annetaan parametrina kentän arvo, ja se palauttaa merkkijonon.

Esimerkki validoijaoliosta:

var validoija = new Validoija("nimi", function(data) {
    if(!data) {
        return "Nimi ei saa olla tyhjä!";
    }

    return "";
});

Jos ylläoleva validoija on lisätty kontrollerille, datan lisäyksen ei tule toimia jos nimikenttä on tyhjä.

Kun kontrolleri tukee validoijien lisäämistä, lisää sinne yllä oleva validoija. Luo myös validoija , joka tarkastaa että aika on muotoa yyyy-MM-dd, esimerkiksi 2012-12-24. Kukin numero saa olla mitä tahansa numeroiden 0 ja 9 välillä. Kannattaa tutustua säännöllisiin lausekkeisiin (google esim. "javascript regular expressions date").

Kun sovelluksesi toimii kuten haluttu, palauta se TMC:lle.

Kontrollerin rooli selainohjelmistoissa

Yllä olevaa sovellusta luodessa huomaamme, että kontrollerin rooli ei ole kovin selkeä. Muistutus-esimerkissä sovelluksessa kontrolleri toimi elementtien rekisterinä siihen asti, kunnes käyttäjä painoi käyttöliittymän nappia. Napin painalluksenkin rekisteröinti tapahtui kontrollerin ulkopuolella. Sovelluksen voi toteuttaa myös ilman kontrolleria siten, että kontrollerin toiminnallisuus sisällytettäisiin alustukseen.

muistutus.init = function() {
    // luodaan palaset
    var listausnakyma = new muistutus.view.Listaus(document.getElementById("tapahtumat"));

    var lista = new muistutus.domain.Tapahtumalista(listausnakyma);
    listausnakyma.listaaTapahtumat(lista.annaTapahtumat());

    document.getElementById("lisaa").addEventListener('click', function() {
        var tapahtuma = {
            nimi: document.getElementById("nimi").value,
            aika: document.getElementById("aika").value
        };

        lista.lisaaTapahtuma(tapahtuma);
        
        document.getElementById("nimi").value = "";
        document.getElementById("aika").value = "";
    }, false);
}

Onko kontrolleri tarpeellinen?

Jos sovelluksessa on useampia näkymiä, joiden välillä haluaisimme siirtyä, kontrollerista on hyötyä. Pienessä sovelluksessa erillisen kontrollerin käyttö saattaa kuitenkin monimutkaistaa sovelluksen rakennetta -- esimerkiksi yllä kontrollerin toiminnallisuuden pystyi lisäämään osaksi init-funktiota. Kontrollereita käytetään myös erityisesti käyttäjän ohjaamiseen useamman näkymän välillä.

Esimerkki: Spoilaaja

Toteutetaan seuraavaksi sovellus, jossa ei ole eksplisiittistä kontrolleria. Sovelluksessa näkymässä tapahtuvat päivitykset siirtyvät mallille näkymään liitettyjen tapahtumankäsittelijöiden kautta. Tapahtumankäsittelijät luodaan sovelluksen alustusvaiheessa, jolloin tapahtumankäsittelijät toimivat siltana mallin ja näkymän välillä. Käytännössä näkymä ei tiedä mallista, eikä malli näkymästä.


   VIEW    <--- näytä ---    MODEL LISTENER
     |                             | 
   muutos                        muutos
     |                             |
VIEW LISTENER --- päivitä -->    MODEL

Sovelluksen aihepiiri on spoilaaja, eli sillä näytetään kirjoihin liittyviä spoilauksia. Hahmotellaan ensin sovelluksen käyttöliittymä. HTML-dokumentin rakenne näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Spoilerit!</title>
    </head>
    <body onload="spoilaaja.init();">
        <header>
            <h1>Spoilerit</h1>
        </header>

        <section id="spoilaukset">
        </section>

        <section id="input">
            <h2>Syötä uusi</h2>
            <label>kirja <input type="text" id="kirja"/></label>
            <label>spoilaus <input type="text" id="spoilaus"/></label>
            <input type="button" id="button" value="Lisää!"/>
        </section>

        <script src="spoilaus.js"></script>
    </body>
</html>

Ohjelmakoodi lisätään nimiavaruuteen spoilaaja.

var spoilaaja = {};

View

Luodaan sovelluksen näkymään liittyvä koodi. Ainoa alue, josta olemme kiinnostuneet, on tunnuksella spoilaukset merkitty alue. Näkymälle asetetaan init-funktiolla alueen tunnus, jonka sisälle se lisää dataa. Kun näkymään lisätään dataa, se luo jokaista kirjaa varten oman tekstikentän, joka sisältää kyseisen kirjan spoilauksen. Kun spoilauksen sisältö muuttuu, kutsutaan erillistä näkymälle asetettavaa tapahtumankuuntelijafunktiota muutokseen liittyvällä datalla.

Tapahtumankuuntelija lisätään sovellukseen sovellusta käynnistettäessä funktiolla setListener. Aina kun näkymään lisätään uutta spoilausta, lisätään tekstikenttään myös tapahtumankuuntelija. Tapahtumankuuntelija lähettää viestin mallille, jos data muuttuu.

spoilaaja.View = function(containerId) {
    var container = document.getElementById(containerId);
    var listener;

    // julkiset metodit
    this.render = function(data, key, value) { 
        if(data !== false) {
            renderAll(data);
        } else {
            renderSingle(key, value);
        }
    }

    this.setListener = function(actionListener) {
        listener = actionListener;
    }
    
    // apufunktiot
    function renderAll(data) {
        clear();

        for (var key in data) {
            renderSingle(key, data[key]);
        }
    }

    function clear() {
        while(container.firstChild) {
            container.removeChild(container.firstChild);
        }
    } 

    function renderSingle(key, value) {
       var element = document.getElementById(key);

       if(!element) {
           // jos elementtiä ei ole vielä olemassa, luodaan sellainen
           createElement(key, value);   
       }

       document.getElementById(key).value = value;
    }

    function createElement(key, value) {
        var article = document.createElement("article");
        var label = document.createElement("label");
        var textField = document.createElement("input");
        textField.type="text";

        label.appendChild(document.createTextNode(key));
        textField.id = key;
        textField.value = value;

        article.appendChild(label);
        article.appendChild(textField);
    
        container.appendChild(article);

        // jokaiseen elementtiin lisätään tapahtumankuuntelija
        if(listener) {
            textField.addEventListener("change", function(eventInformation) {
                var textField = eventInformation.target;
                listener(textField.id, textField.value);
            }, false);
        }
    }
}

Seuraavaksi datan säilytys ja esitys.

Model

Model kapseloi sovelluksessa käytettävän datan sisäänsä. Tämän lisäksi se tarjoaa aksessorit dataan, sekä tapahtumankäsittelijäfunktion, jota kutsutaan, jos mallin sisältämään dataan tehdään muutoksia. Sovelluksen sisältämä data on käytännössä olio, eli joukko avain-arvo -pareja. Avain on aina kirjan nimi, ja arvo kirjaan liittyvä spoilaus.

spoilaaja.Model = function(initialData) {
    var data = initialData;
    var listener;
    
    this.update = function(key, value) {
        data[key] = value;

        if(!listener) {
            console.log("Model update called, but listener has not been set :("); 
            return;
        }

        listener(key, value);
    }

    this.get = function(key) {
        return data[key];
    }

    this.getAll = function() {
        return data;
    }

    this.setListener = function(action) {
        listener = action;
    }
}

Tässä vaiheessa saatat huomata, että ylläolevassa Model-toteutuksessa ei ole minkäänlaista viittausta sovelluksen käyttötarkoitukseen. Tämä on tarkoituskin. Itseasiassa, ylläolevaa toteutusta voisi käyttää monissa muissakin yhteyksissä...

Sovelluksen alustaja

Luodaan seuraavaksi sovelluksen alustaja. Alustajan tehtävänä on alustaa sovellus, ja kytkeä näkymä ja malli toisiinsa tapahtumankäsitelijöiden kautta. Luodaan ensin sovelluksessa käytettävä data, jonka jälkeen lisätään näkymälle tapahtumankuuntelija. Näkymään liitettävä tapahtumankuuntelija kutsuu mallin update-funktiota jos käsitelty data on muuttunut. Huomaa, että tapahtumalogiikka ei ole osa näkymää, vaan osa sovelluksen alustusta.

spoilaaja.init = function() {
    var data = {};
    data["Running Blind"] = "Murhaaja on nainen!";
    data["Cat's cradle"] = "se jäätävä homma...";
 
    var model = new spoilaaja.Model(data);
    var view = new spoilaaja.View("spoilaukset");

    // tapahtuman kuuntelijat viewlle (näin alustava tulostus ei muuta modelia
    view.setListener(function(key, value) {
        console.log("Listener in view called");
        if(model.get(key) !== value) {
            model.update(key, value);
        }
    });

Kun näkymässä on toiminnallisuus mallin päivittämiseen, kutsutaan näkymän render-metodia. Metodi render luo näkymän annetun datan pohjalta.

    view.render(model.getAll(), false, false);

Lisätään vielä tapahtumankuuntelija modelille. Nyt jos modelissa tapahtuu muutos, se päivitetään myös näkymälle. Tällöin näkymä saa tietoonsa muutokset, jotka tapahtuvat muualla. Luotava tapahtumankuuntelija ja kuuntelijan käsittelyn toteutus mallissa aiheuttaa käytännössä sen, että näkymä saa tietoonsa mallin muutokset.

    model.setListener(function(key, value) {
        view.render(false, key, value);
    });

Kytketään lopuksi käyttöliittymässä olevaan nappiin spoilauksen lisäystoiminnallisuus.

    // kytketään nappi toimimaan
    var nappi = document.querySelector("#input #button");
    nappi.addEventListener("click", function(eventInformation) {
        var kirja = document.querySelector("#input #kirja").value;
        if(!kirja) {
            return;
        }
        var spoilaus = document.querySelector("#input #spoilaus").value;

        model.update(kirja, spoilaus);
    }, false);
}

Observer Pattern

Suunnittelumallia, jossa komponenttiin voi lisätä muiden komponenttien kutsufunktioita siten, että niitä kutsutaan komponentin päivittyessä kutsutaan Observer Patterniksi. Käytännössä komponentti ei tiedä muista komponenteista mitään, sillä on vain pääsy niiden tarjoamassa rajapinnassa olevaan yksittäiseen kutsufunktioon.

Spoilaajan Backend (2p)

Muistellaan taas JSON-kyselyjen tekemistä palvelimelle. Käy kertaamassa kappale 8 ennen tätä tehtävää. Tehtävänäsi on tässä lisätä edellä esitettyyn ohjelmaan backend-toiminnallisuus. Backend-toiminnallisuuden tehtävänä on sovelluksen käynnistyessä hakea olemassaolevat spoilaukset palvelimelta, sekä lähettää palvelimelle mahdolliset muutokset.

Spoilausten tallentamiseen käytetty palvelinsovellus toimii osoitteessa http://bad.herokuapp.com/app/. Kannattaa tehdä kysely palvelimelle selaimella ennen tehtävän tekemistä, jotta palvelin on varmasti käynnissä. Palvelin tarjoaa osoitteessa http://bad.herokuapp.com/app/spoilers/ toimivan rajapinnan. Kun rajapintaan tehdään GET-kysely, palvelin palauttaa kaikki tallennetut spoilaukset.

Uusien spoilausten lisääminen tai olemassaolevien muokkaaminen tapahtuu POST-pyynnöllä rajapintaan. POST-pyyntö tehdään siten, että sen mukana lähetetään JSON-muotoista dataa merkkijonomuodossa. JSON-datan tulee näyttää seuraavalta oliomuodossa:

var lahetettava = {
    name: "spoilattava",
    spoiler: "spoilaus"
};

Toteuta sovellus aluksi niin, että toteutat vain Backend-komponentin, joka tallettaa dataa. Kun saat sovelluksen kommunikoimaan backend-komponentin kanssa, lisää backendille viestien lähetys ja vastaanottaminen palvelimelle. Tässä kohdassa voi olla hyödyllistä tutustua synkronoituihin XMLHttpRequest-pyyntöihin (google esim. "synchronous xmlhttprequest send").

MVC, MVP, MVVM, ...

Käytännössä kaikissa MV* -suunnittelumalleissa ajatuksena on näkymän ja sovelluslogiikan erottaminen toisistaan. Näimme aiemmin MVC-mallin, jossa pyyntö käytännössä kulkee näkymältä kontrollerille, joka ohjaa pyynnön mallille. Malli taas päivittää näkymää tarpeen vaatiessa. Käytännössä pyynnön kulku MVC-mallissa näyttää seuraavalta:

                  kliksu
                    ||
                    \/
                   VIEW
               /         /\  
              /           \ 
ohjauspyyntö /             \  päivitys
            /               \
           \/                \
  CONTROLLER  - muokkaus - >  MODEL
            

Kaksi viime aikoina päätänsä nostanutta MVC-varianttia ovat MVP (Model, View, Presenter) ja MVVM (Model, View, ViewModel). Tutustutaan niihin pikaisesti.

MVP

Joissain tapauksissa sovelluksissa ei ole suoraa mahdollisuutta näkymän ja mallin toisiinsa kytkemiseen. Tällöin sovellus tarvitsee näkymän ja mallin välille erillisen komponentin, joka ohjaa pyyntöjä näkymältä malliin ja mallilta näkymään. ASCII-kaaviona sovelluksen MVP näyttää seuraavalta:

               kliksu
                 ||
                 \/
                VIEW
                / /\ 
               /  /
              /  / 
ohjauspyyntö /  /  päivitys
            /  /
           \/ /               
        PRESENTER
            \  /\
             \  \
              \  \
    muokkaus   \  \   muutos/tapahtuma
                \  \
                 \  \
                 \/  \
                  MODEL

Voimme muokata aiemmin tekemäämme muistutussovellusta seuraamaan MVP-mallia poistamalla mallilta riippuvuuden näkymään, ja lisäämällä kontrolleriin päivityksen tekemisen näkymälle. Käytännössä Presenter-oliolle tulisi lisätä myös tapahtumankuuntelija, jota model voisi kutsua tarvittaessa.

MVVM

MVVM on muunnos MVP:hen, jossa ViewModel on mallista valittua näkymää varten muokattu esitys käytössä olevasta datasta. ViewModel on kytkeytynyt näkymään näkymän tarjoaman funktion kautta. Kun ViewModelissa oleva data muuttuu, se kutsuu näkymän tarjoamaa funktiota siten, että näkymä päivittää itsensä ViewModel-olion pohjalta.

               kliksu
                 ||
                 \/
                VIEW
                / /\ 
               /  /
              /  / 
ohjauspyyntö /  /  tapahtumakutsu
            /  /
           \/ /               
        VIEWMODEL
            \  /\
             \  \
              \  \
    muokkaus   \  \   muutos/tapahtuma
                \  \
                 \  \
                 \/  \
                  MODEL

Valmiit JavaScript-kirjastot

Osa aiemmin toteuttamistamme ohjelmista ei toimi kaikilla nykyaikaisilla selaimilla. Osassa taas toistetaan samoja asioita uudestaan ja uudestaan. Yhteensopivuusongelmat johtuvat suurelta osin selainvalmistajien heikosta standardien seuraamisesta, ja innottomuudesta vanhempien selainten päivittämiseen. Selainohjelmistoja kehitettäessä tulee huomioida myös vanhempien selainten käyttäjät -- sovelluksen tilaajan määrittelemään pisteeseen asti.

Selainohjelmistojen tekemiseen on huomattava määrä valmiita kirjastoja, joiden yksi tarkoitus on poistaa joidenkin selainten tietynlaiset JavaScript-syntaksin vaatimukset. Kirjastot tarjoavat myös apufunktioita toistuvan koodin ja toiminnallisuuden vähentämiseen. Mielenkiintoista JavaScript-kirjastojen ilmentymisessä on se, että JS-yhteisössä on havaittavissa samanlaista käyttäytymistä kuin palvelinpuolen yhteisöissä muutamia vuosia sitten.

Kyllähän se kirjasto xxx on parempi kun se tekee tän yhdellä rivillä, sun sovelluskehyksellä menee seitsemän!

Aivan kuten ohjelmointikielten tapauksessa, tietyt ohjelmistokirjastot sopivat joihinkin asioihin paremmin, toiset toisiin. Tutustutaan seuraavaksi tällä hetkellä ehkäpä eniten käytettyyn JavaScript-kirjastoon, jQueryyn.

jQuery

jQuery on JavaScript-kirjasto, jonka tarkoitus on helpottaa selainohjelmistojen toteutusta. Se tarjoaa tuen mm. DOM-puun muokkaamiseen, tapahtumien käsittelyyn sekä palvelimelle tehtäviin kyselyihin, ja sen avulla toteutettu toiminnallisuus toimii useimmissa selaimissa.

Uusimman jQuery-version saa ladattua täältä. Käytännössä jQuery on JavaScript-tiedosto, joka ladataan sivun latautuessa. Tiedoston voi asettaa esimerkiksi head-elementin sisään, tai ennen omia lähdekooditiedostoja.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    	<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
     </head>
    <body>

        <!-- sivun sisältö -->

        <script src="javascript/jquery-1.8.2.min.js"></script>
        <script src="javascript/koodi.js"></script>
    </body>
</html>

Valitsimet

Olemme tähän mennessä käyttäneet valmiita JavaScriptin DOM-toiminnallisuuksia. Elementtien etsimiseen on käytetty mm. Selectors APIn querySelector-kutsua, esimerkiksi komennolla var elementti = document.querySelector("#nimi"); haetaan elementti, jonka tunnus on "nimi". JQuery käyttää Sizzle-kirjastoa elementtien valinnan helpottamiseen. Esimerkiksi sivun elementti, jonka tunnus on "nimi", löytyy seuraavalla komennolla.

var elementti = $("#nimi");

Kyselyiden formaatti on siis $("kysely"), missä kysely on hyvin samankaltainen kuin aiemmin käyttämämme Selector APIn kyselyrajapinta. Vastaavasti kaikki header-elementissä olevat a-elementteihin löytyy komennolla.

var elementit = $("header a");

Myös tietyn luokan toteuttavien elementtien haku on helppoa. Alla olevassa esimerkissä on kolme tekstikenttää, joista 2 on piilotettu. Piilotettujen tekstikenttien tyyliluokka on jquery-dom-1-hidden.

text 1

text 2

text 3

Huomaa, että koodi toimii, sillä jQuery on ladattu osaksi tätä sivua. Huomaa myös, että ylläolevan koodin voi tehdä huomattavasti tehokkaammin.

jQueryn valitsimet

Tarkempi kuvaus jQueryn valitsimista löytyy osoitteesta http://api.jquery.com/category/selectors/.

 

DOM-puun muokkaus

JQuery lisää DOM-puun elementteihin toiminnallisuuksia, jotka helpottavat DOM-puun muokkausta. Esimerkiksi metodi removeClass poistaa elementiltä tai kokoelmalta elementtejä halutun luokan. Alla on sama esimerkki kuin yllä, mutta nyt piilotettujen elementtien tyyliluokka on jquery-dom-2-hidden.

text 1

text 2

text 3

Yllä olevassa esimerkissä haetaan kaikki elementit, joiden tyyliluokka on "jquery-dom-2-hidden", ja poistetaan niiltä haluttu tyyli. Koska uudet toiminnallisuudet on lisätty elementteihin, voidaan kyselyt myös ketjuttaa. Alla haetaan ensin kaikki elementit, joiden tyyliluokka on jquery-dom-3-hidden, jonka jälkeen haluttu tyyliluokka poistetaan.

text 1

text 2

text 3

Kyselyiden avulla voidaan luoda myös monimutkaisen näköisiä rakenteita. Alla haetaan kaikki body-elementin sisällä olevat solmut, joilla ei ole tunnusta "jquery-dom-4-js-esim", ja jotka eivät ole sen alla olevia textare tai input-elementtejä. Kun solmut on haettu, solmuille lisätään tyyliluokka "hidden".

Jos klikkaat ylläolevaa nappia, joutunet muokkaamaan koodia jotta saat sivun takaisin näkyville.

DOM-puun muokkaus

Tarkempi kuvaus operaatioista DOM-puun muokkaamiseen löytyy osoitteesta http://api.jquery.com/category/Manipulation/.

 

Tapahtumien käsittely

JQuery rakentaa JavaScriptin valmiiden komponenttien päälle, joten sillä on toiminnallisuus myös tapahtumankäsittelijöiden rekisteröimiseen sivun komponenteille. Tutkitaan seuraavaa jo tutuhkoa HTML-dokumenttia.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title>Kindler</title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body onload="init();">
        <header>
            <h1>Kindler</h1>
            
            <nav>
                <a href="#">Eka artikkeli</a>
                <a href="#">Toka artikkeli</a>
                <a href="#">Kolmas artikkeli</a>
            </nav>
        </header>

	<section>
          <article>
	    <h1>Eka artikkeli</h1>
	    
	    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
	  </article>
	  
          <article>
	    <h1>Toka artikkeli</h1>
	    
	    <p>Morbi a elit enim, sit amet iaculis massa. Vivamus blandit...</p>
	  </article>
	  
          <article>
	    <h1>Kolmas artikkeli</h1>
	    
	    <p>Now that we know who you are, I know who I am. I'm...</p>
	  </article>
	</section>

        <!-- lähdekooditiedostojen lataus -->
	<script type="text/javascript" src="javascripts/jquery-1.8.2.min.js"></script>
        <script type="text/javascript" src="javascripts/koodit.js"></script>
    </body>
</html>

Olemme aiemmin määritelleet tapahtumankäsittelyn osana HTML-dokumenttia siten, että JavaScript-kutsut on lisätty erillisessä init-metodissa (unohdamme MVC-mallin hetkeksi esimerkin yksinkertaistamiseksi). Alla oleva lähdekoodi käyttää aiemmin oppimaamme querySelector-toteutusta siihen, että tapahtumankäsittelijät lisätään vain menuvalikon linkkeihin. Kutsu preventDefault() estää linkin seuraamisen.

function init() {
    var navLinks = document.querySelectorAll("header nav a");
    for(var i = 0; i < navLinks.length; i++) {
        var link = navLinks[i];

        // lisätään elementille id, josta päätellään näytettävä artikkeli
        link.id = i;

        // lisätään tapahtumankuuntelija tapahtumalle click. huom! ero onclick-attribuuttiin
        link.addEventListener('click', function(eventInformation) {
            var origin = eventInformation.target;

            // kutsutaan erillistä displayArticle-funkiota, joka
            // näyttää halutun artikkelin
            displayArticle(origin.id);

            // kielletään selainta tekemästä oletustoiminto (siirtyminen)
            eventInformation.preventDefault();
        }, false);
    }

    // ...
}

function displayArticle(index) {
    var articles = document.getElementsByTagName("article");

    for(var i = 0; i < articles.length; i++) {
        if (index == i) {
            articles[i].className='';
        } else {
            articles[i].className='hidden';
        }
    }
}

Muokataan ylläolevaa esimerkkiä siten, että käytämme JQueryä.

Muokataan ensin funktio displayArticle toiminnallisuutta. Toteutetaan se siten, että funktiokutsussa piilotamme aina ensin kaikki artikkelit, jonka jälkeen näytämme indeksin määräämän artikkelin. Valitsimella ":eq(indeksi)" voimme valita elementin tietystä indeksistä.

function displayArticle(index) {
    $("article").addClass("hidden");
    $("article:eq(" + index + ")").removeClass("hidden");
}

Lähdetään seuraavaksi pilkkomaan function init-toiminnallisuutta. Asetetaan funktion init toiminnallisuus ensin $(document).ready(function(){ });-lohkon sisään. Lohkon sisältö suoritetaan kun sivun lataaminen on valmis -- emme enää tarvitse body-elementin onload-attribuuttia. Koodi näyttää nyt seuraavalta:

$(document).ready(function(){
    var navLinks = document.querySelectorAll("header nav a");
    for(var i = 0; i < navLinks.length; i++) {
        var link = navLinks[i];

        // lisätään elementille id, josta päätellään näytettävä artikkeli
        link.id = i;

        // lisätään tapahtumankuuntelija tapahtumalle click. huom! ero onclick-attribuuttiin
        link.addEventListener('click', function(eventInformation) {
            var origin = eventInformation.target;

            // kutsutaan erillistä displayArticle-funkiota, joka
            // näyttää halutun artikkelin
            displayArticle(origin.id);

            // kielletään selainta tekemästä oletustoiminto (siirtyminen)
            eventInformation.preventDefault();
        }, false);
    }

    // ...
});

function displayArticle(index) {
    $("article").addClass("hidden");
    $("article:eq(" + index + ")").removeClass("hidden");
}

Luodaan ensin toiminnallisuus, jolla linkkeihin asetetaan tunnus-attribuutit. JQueryssä on kätevä kokoelman iterointiin tarkoitettu each-komento, joka saa JQueryltä parametrinaan iteroitavan elementin indeksin. Muuttuja $(this) viittaa kyseisellä hetkellä läpikäytävään muuttujaan. Allaoleva komento käy läpi jokaisen linkki-elementin, ja kutsuu kullekin each-komennolle parametrina antamaamme funktiota. Komento attr asettaa (tai hakee jos toista parametria ei määritellä) elementin komennossa määritellyn attribuutin.

    $("header nav a").each(function(index) {
        $(this).attr("id", index);
    });

Lisätään seuraavaksi jokaiselle linkille tapahtumankäsittelijä. Komento click auttaa tässä huomattavasti. Voimme hyödyntää myös aiemmin huomaamaamme each-komentoa. Alla lisäämme jokaiseen linkkiin funktion, joka kuuntelee klikkausta. Funktion sisältö lienee tuttu.

    $("header nav a").each(function(index) {
        $(this).click(function(eventInformation) {
            displayArticle(eventInformation.target.id);
            eventInformation.preventDefault();
        });
    });

Each-komento tarjoaa meille indeksin, joten muokataan edellistä vielä hieman. Käytetään suoraan each-komennon tarjoamaa indeksiä artikkelin näyttämiseen.

    $("header nav a").each(function(index) {
        $(this).click(function(eventInformation) {
            displayArticle(index);
            eventInformation.preventDefault();
        });
    });

Huomaamme vielä, että voimme yhdistää linkin tunnuksen lisäämisen ja tapahtumankäsittelyn lisäämisen saman each-komennon sisään.

    $("header nav a").each(function(index) {
        $(this).attr("id", index);
        $(this).click(function(eventInformation) {
            displayArticle(index);
            eventInformation.preventDefault();
        });
    });

Koodimme näyttää nyt kokonaisuudessaan seuraavalta:

$(document).ready(function(){

    // sattumalta saa parametrina indeksin
    $("header nav a").each(function(index) {
        $(this).attr("id", index);
        $(this).click(function(eventInformation) {
            displayArticle(index);
            eventInformation.preventDefault();
        });
    });
});

function displayArticle(index) {
    $("article").addClass("hidden");
    $("article:eq(" + index + ")").removeClass("hidden");
}

HTML-dokumentista on myös poistettu body-elementin onload-attribuutti.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title>Kindler</title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body>

        <!-- sama sisältö kuin aiemminkin -->

	<script type="text/javascript" src="javascripts/jquery-1.8.2.min.js"></script>
        <script type="text/javascript" src="javascripts/koodit.js"></script>
    </body>
</html>

JQueryMOOC

Muokkaa tehtäväpohjan mukana tulevaa tehtävistä W1E08 ja W2E02 tuttua MOOC-sivua siten, että elementtien näyttäminen ja piilottaminen toteutetaan JQueryn tarjoamien apuvälineiden avulla.

Poista lopulta myös turhaksi tullut myös turha body-elementin onload-attribuutti.

Kun olet vaihtanut toteutuksen JQueryksi, ja toteutuksen toiminnallisuus on ennallaan, palauta tehtävä TMC:lle.

Kyselyt palvelimelle

JQuery tarjoaa myös tuen kyselyjen tekemiseen erilliselle palvelinkomponentille.

JSONP

JSONP-kyselyt, eli kyselyt, joissa vastauksena tulee selaimessa evaluoitavaa JavaScript-koodia hoituvat kätevästi JQueryn $.getJSON-funktiolla. Alla olevassa esimerkissä haemme Twitterin JSONP-apista rageresearch-tunnuksen lähettämiä viestejä. JQuery määrittelee callback-funktion itse, ja asettaa sen osoitteeseen jätettävän kysymysmerkin kohdalle.

Kyselyn palauttama data ohjataan $.getJSON-funktion toisena parametrina määriteltävään funktioon. Alla olevassa esimerkissä kutsumme vain alert-komentoa.

$.getJSON("http://api.twitter.com/1/statuses/user_timeline/rageresearch.json?count=4&callback=?",
    function(data) {
        alert(data);
    }
);

Ylläoleva esimerkki palauttaa listan olioita. Käytetään JQueryn each-komentoa listassa olevien elementtien iterointiin. Komennolle each voi antaa parametrina iteroitavan listan, sekä funktion, jota kutsutaan jokaisella listassa olevalla oliolla.

$.getJSON("http://api.twitter.com/1/statuses/user_timeline/rageresearch.json?count=4&callback=?",
    function(data) {
        $.each(data, function(i, item) {
            alert(i + ": " + item);
        });
    }
);

Nyt ylläoleva komento tulostaa oliot yksitellen. Oletetaan, että käytössämme on elementti, jonka tunnus on "viestit". JQuery tarjoaa myös mahdollisuuden nopeaan tekstielementtien luontiin komennolla $("<p/>"). Elementteihin voi asettaa tekstin text-komennolla, ja elementin voi lisätä tietyllä tunnuksella määriteltyyn elementtiin komennolla appendTo("#tunnus").

$.getJSON("http://api.twitter.com/1/statuses/user_timeline/rageresearch.json?count=4&callback=?",
    function(data) {
        $.each(data, function(i, item) {
            $("<p/>").text(item.text).appendTo("#viestit");
        });
    }
);

XMLHttpRequest

Jos tiedämme, että palvelu palauttaa JSON-dataa, voimme käyttää yllä käsiteltyä lähestymistapaa. Esimerkiksi viestien noutaminen Chat-chat -tehtävän viestipalvelimelta onnistuu seuraavalla komennolla. Tässä tapauksessa lisäämme jokaiseen viestiin liittyvän message-attribuutin "viestit" -tunnuksella määriteltyyn elementtiin.

$.getJSON("http://bad.herokuapp.com/app/messages", function(data) {
    $.each(data, function(i, item) {
        $("<p/>").text(item.message).appendTo("#viestit");
    });
});

Yllä oleva komento on lyhenne alla määritellystä komennosta.

$.ajax({
    url: "http://bad.herokuapp.com/app/messages",
    dataType: 'json',
    success: parseMessages
});

function parseMessages(messages) {
    $.each(messages, function(i, item) {
        $("<p/>").text(item.message).appendTo("#viestit");
    });
}

Komennolle $.ajax voi lisätä myös dataa, mitä lähetetään palvelimelle. Esimerkiksi seuraavalla komennolla lähetetään osoitteeseen http://bad.herokuapp.com/app/in olio, jonka sisällä on attribuutit name ja details. Lähetettävän datan tyyppi asetetaan attribuutilla contentType, alla ilmoitamme että data on json-muotoista, ja että se käyttää utf-8 -merkistöä.

var dataToSend = JSON.stringify({
        name: "bob",
        details: "i'm ted"
    });

$.ajax({
    url: "http://bad.herokuapp.com/app/in",
    dataType: 'json',
    contentType:'application/json; charset=utf-8',
    type: 'post',
    data: dataToSend
});

Pyynnössä voi myös sekä lähettää, että vastaanottaa dataa. Attribuutin success asettaminen ylläolevaan pyyntöön aiheuttaa success-attribuutin arvona olevan funktion kutsun kun pyyntö on onnistunut.

JQuerySpoilaajanBackend

Toteuta Spoilaajan Backend-tehtävä tässä JQueryn avulla. Jos hyödynsit aiemmin synkronisia kutsuja, kannattaa hyödyntää niitä myös tässä. Vinkki: $.ajax-komennolle asettaa attribuutin async: false, jolloin tulee myös määritellä success-attribuutille funktio, jota kutsutaan kun data on noudettu.

Näkymätemplatet ja Mustache.js

Selainohjelmistojen rakennetta suunniteltaessa yksi huolenaihe on sivun eri näkymien järkevä hallinta. Aiemmin näkemässämme esimerkissä sivun osia piilotetaan ja näytetään dynaamisesti. Toinen pulma liittyy HTML-koodin generointiin datan pohjalta: generointi on auttamatta työlästä.

Näkymätemplatet ovat HTML-koodipätkiä, jotka sisältävät halutun HTML-rakenteen sekä paikat datalle. Eräs projekti näkymätemplatejen generointiin on

Mustache.js, joka on mustache-projektin osa.

Käytännössä näkymätemplatejen käyttö toimii siten, että HTML-dokumenttiin piilotetaan osa, joka sisältää kaikki HTML-templatet. Kun käyttäjä esimerkiksi klikkaa linkkiä, renderöidään näkyvälle alueelle templaten ja datan pohjalta uusi sisältö. Tutkitaan alla olevaa esimerkkiä, jossa on erillinen osio tunnuksella "view", ja toinen piilotettu osio, joka sisältää templatet.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" ></meta>
    	<title></title>
        <link rel="stylesheet" href="style.css" type="text/css" />
    </head>
    <body>
	<section id="view">
	</section>

	<div class="hidden">
	    <div id="template">
	        <article>
	            <h2>{{nimi}} {{ika}}</h2>
                </article>
	    </div>
	</div>

        <!-- lähdekooditiedostojen lataus, käytössä vain mustache.js -->
	<script type="text/javascript" src="javascripts/jquery-1.8.2.min.js"></script>
	<script type="text/javascript" src="javascripts/mustache.js"></script>
        <script type="text/javascript" src="javascripts/koodi.js"></script>
    </body>
</html>

Tyylitiedosto style.css sisältää vain yhden määrittelyn.

.hidden {
    display: none;
}

Lähdekooditiedosto koodi.js sisältää seuraavanlaisen lähdekoodin.

$(document).ready(function() {
    var data = {
        nimi: "Bob",
        ika: function() {
            return 14+7;
        }
    };

    var html = Mustache.render($("#template").html(), data);
    $("#view").html(html);
});

Tutkitaan koodia hieman tarkemmin. Komennolla $(document).ready(function() { ... }); määritellään toiminnallisuus, joka suoritetaan kun sivu on latautunut. Toiminnallisuus sisältää olion luomisen. Luomme olion, jolla on attribuutit nimi (arvo "Bob"), ja ika (arvo funktion palauttama arvo).

Tämän jälkeen oleva koodi ei olekaan vielä tuttua. JQuery tarjoaa pääsyn elementin html-koodiin komennolla html, eli komento $("#template").html() palauttaa tunnuksella "template" merkityn elementin sisältämän HTML-koodin. Tämä koodi annetaan Mustachen render-komennolle yhdessä data-olion kanssa. Mustachen render-komento asettaa olion data sisällön HTML-koodissa merkityille paikoille, ja palauttaa uuden merkkijonon. Merkkijono sisältää HTML-koodin, jossa merkityt kohdat sisältävät nyt datasta saadut arvot.

Lopuksi html-koodi asetetaan tunnuksella "view" määritellyn elementin sisään.

Listojen läpikäynti

Listojen läpikäynti onnistuu {{#muuttuja}}-operaattorilla, joka aloittaa läpikäynnin. Läpikäynnin lopetus tapahtuu {{/muuttuja}}-operaattorilla. Esimerkiksi alla on määritelty messages-attribuutissa olevien olioiden läpikäynti siten, että jokaiselta oliolta kutsutaan attribuuttia nickname ja message.

	<div class="hidden">
	    <div id="template">
                <article>
                    {{#messages}}
                        <p><strong>{{nickname}}:</strong> {{message}}</p>
                    {{/messages}}
               </article>
            </div>
        </div>

Voimme kytkeä ylläolevan templaten helposti Chat-chat -tehtävän viestit hakevaan palveluun.

function parseMessages(messages) {
    var data = {
        messages: messages
    };

    var html = Mustache.render($("#template").html(), data);
    $("#view").html(html);
}

$(document).ready(function() {
    $.ajax({
        url: "http://bad.herokuapp.com/app/messages",
        dataType: 'json',
        success: parseMessages
    });
});

Ehkäpä tärkein muistettava ylläolevassa esimerkissä on se, että data tulee antaa Mustachelle olion sisällä. Jos render-komentoa kutsutaan suoraan messages-oliolla, joka sisältää listan chat-viestejä, ei Mustache tiedä miten toimia.

Movember (2p)

Muokkaa tehtäväpohjassa olevaa dokumenttia siten, että näkymä generoidaan tarvittaessa HTML-templateista. Tiedostossa code.js olevaan muuttujaan data on asetettu valmiiksi HTML-dokumentissa tällä hetkellä olevat sisällöt. Tehtävänäsi on muokata tiedostoa index.html siten, että se sisältää template-elementin, jonka pohjalta sivun näkymiä (perusmooc, materiaali, ...) voidaan luoda.

Muokkaa myös tiedostoa code.js siten, että kukin näkymä generoidaan aina linkkiä klikattaessa. Generoitu näkymä tulee asettaa osaksi näkymäaluetta, jonka joudut myös luomaan. Kannattaa hyödyntää aiemmin tekemääsi JQueryMOOC-pohjaa.

Lykkyä tykö!

Templatet piiloon script-elementin sisään!

Piilotimme templatet aiemmin div-elementin sisään siten, että div-elementille oli asetettu tyyli, joka piilottaa sen sisällön. Tyypillisempi lähestymistapa templatejen piilottamiseen liittyy selainten käyttäytymisen script-elementin kanssa hyödyntämiseen. Jos selain ei tunne script-elementille annettavaa tyyppiä, ei se myöskään yritä tulkata elementin sisältöä. Templaten voi asettaa myös script-elementin sisälle seuraavasti:

    <script id="template" type="text/html">
        <article>
            {{#messages}}
                <p><strong>{{nickname}}:</strong> {{message}}</p>
            {{/messages}}
       </article>
    </script>

Tämä mahdollistaa myös paremman kuvaelementtien sisällyttämisen templateihin. Kun selain ei yritä tulkita templatea, se ei myöskään yritä hakea templatessa mahdollisesti olevaa kuvaviitettä.

Case: Henkilötiedot

Rakennetaan sovellus, jossa käyttäjä voi etsiä henkilöitä ja katsoa yksittäisen henkilön tietoja. Henkilöiden etsiminen tapahtuu kirjoittamalla tekstikenttään osa henkilön nimestä, tai henkilön nimi kokonaan. Henkilön nimeä kirjoitettaessa sovellus ehdottaa vaihtoehtoja. Kun oikea vaihtoehto löydetään, käyttäjä voi klikata nimeä ja katsoa henkilöön liittyviä tietoja.

Käytössämme on seuraavanlainen datajoukko.

var data = [{ "id": 1, "name": "homer", "age": 44 },
  { "id": 2, "name": "bart", "age": 12 },
  { "id": 3, "name": "maggie", "age": 4 },
  { "id": 4, "name": "lisa", "age": 10 },
  { "id": 5, "name": "marge", "age": 43 },
  { "id": 6, "name": "abraham", "age": 85 },
  { "id": 7, "name": "mona", "age": 81 },
  { "id": 8, "name": "amber", "age": 51 }]

Henkilöiden listaaminen

Jotta henkilöä voi etsiä, tulee sovelluksessa olla tekstikenttä nimen täyttämiseen, sekä alue henkilöiden näyttämiseen. Luodaan sovellukselle elementit etsimiseen ja tulosten listaamiseen.

  <section>
    <input type="text" id="searchbox"/>
  </section>

  <section id="resultview"></section>

Tunnuksella searchbox merkittyä elementtiä käytetään tekstin syöttämiseen, ja tunnuksella resultview merkittyä elementtiä tulosten näyttämiseen. Luodaan aivan ensin toiminnallisuus datajoukon näyttämiseen osana tuloslistaa. Käytetään tähän templatea.

  <script id="searchresulttemplate" type="text/html">
    {{#list}}
    <p>{{name}}</p>
    {{/list}}
  </script>

Yllä oleva template käy listan nimeltä list läpi siten, että se tulostaa jokaisen listalla olevan elementin attribuutin name p-elementin sisään. Käytännössä sille tulee siis antaa olio, jolla on attribuutti list, jotta se voi tulostaa alkiot.

var data = [{ "id": 1, "name": "homer", "age": 44 },
  { "id": 2, "name": "bart", "age": 12 },
  { "id": 3, "name": "maggie", "age": 4 },
  { "id": 4, "name": "lisa", "age": 10 },
  { "id": 5, "name": "marge", "age": 43 },
  { "id": 6, "name": "abraham", "age": 85 },
  { "id": 7, "name": "mona", "age": 81 },
  { "id": 8, "name": "amber", "age": 51 }]

// huom! attribuutti list sisältää datan
var dataForTemplate = {
  list: data
}

Luodaan toiminnallisuus, jonka avulla lista näytetään kun käyttäjä on kirjoittanut jotain tekstialueeseen. Käytetään tähän keyup-komentoa.

$(document).ready(function() {
    $("#searchbox").keyup(function() {
      showdata();
    });
});

Funktio showdata näyttää datan sivulla. Luodaan aivan ensiksi funktio, joka näyttää aina koko datan. Jotta datan renderöinti onnistuu aiemmin määrittelemämme templaten avulla, tulee data asettaa erillisen olion attribuutin list arvoksi.

function showdata() {
  var dataForTemplate = {
    list: data
  }

  // renderöidään tulokset mustachen avulla       
  var html = Mustache.render($("#searchresulttemplate").html(), dataForTemplate);
  // näytetään tulokset
  $("#resultview").html(html);
}

Nyt sivumme näyttää tulokset.

Henkilöiden filtteröinti

Toteutetaan seuraavaksi henkilöiden filtteröinti. Filtteröinnin toteutus onnistuu helpohkosti käyttämällä edellä olevaa lähestymistapaa, ja muokkaamalla muuttujaa dataForTemplate JQueryn valmiin grep-komennon avulla. Komennolle grep annetaan parametrina funktio, joka saa grep-komennolta sille parametrina annetusta listasta aina yksittäisen alkion ja sen indeksin. Funktion tulee palauttaa arvo true tai false riippuen siitä halutaanko alkio säilyttää. Alla olevassa esimerkissä tarkistetaan onko parametrina saadun alkion attribuutti nimi "Mikke".

data = $.grep(data, function(element, index) {
  return element.name === "Mikke";
});

Omassa toteutuksessamme haluamme etsiä henkilöä, jonka nimessä on searchbox-elementissä oleva arvo. Koska käsittelemämme lista on muuttujan dataForTemplate attribuutti, annetaan grep-komennolla parametrina kyseinen attribuutti.

function showdata() {
  var dataForTemplate = {
    list: data
  }

  // filtteröinti
  var mustContain = $("#searchbox").val();
  dataForTemplate.list = $.grep(dataForTemplate.list, function(person, index) {
    return person.name.indexOf(mustContain) != -1;
  });  

  // renderöidään tulokset mustachen avulla       
  var html = Mustache.render($("#searchresulttemplate").html(), dataForTemplate);
  // näytetään tulokset
  $("#resultview").html(html);
}

Cannot call method 'search' of undefined

Jos Mustache näyttää virheen "Uncaught TypeError: Cannot call method 'search' of undefined", tarkista aivan ensin että olet asettanut templatejen tunnukset oikein. Virhe "TypeError: this.tail.search is not a function" viittaa taas usein käytettävän templaten sisällön ja datan sisällön epäjohdonmukaisuuteen -- tarkista tässä myös että annat oikeasti html:ää Mustachelle..

 

WorldClock

Tehtäväpohjan mukana tulee tiedosto data.json, jossa on eri alueiden aikavyöhykkeitä. Toteuta sovellus aikavöhykkeiden hakemiseen. Aikavyöhykkeiden hakemisen tulee tapahtua siten, että kun käyttäjä kirjoittaa tekstiä sovelluksessa olevaan tekstikenttään, sovellus etsii paikkoja johon haku käy. Toteuta haku siten, että kirjainkoolla ei ole väliä. Komento toLowerCase() lienee tässä hyödyllinen, data kannattaa hakea esimerkiksi $.getJSON-kutsulla.

Kun sovelluksesi toimii kuten alla, palauta se TMC:lle. Varmista, että molemmat haut onnistuvat!

Henkilön valintatoiminnallisuus

Lisätään seuraavaksi mahdollisuus henkilön valintaan. Jotta henkilön voi valita haun näyttämästä listasta, tulee listassa olevalla elementillä olla jonkinlainen tunnistusmahdollisuus. Lisätään templateen attribuutti "data-id", johon asetetaan aina henkilön oma tunnus.

  <script id="searchresulttemplate" type="text/html">
    {{#list}}
    <p data-id="{{id}}">{{name}}</p>
    {{/list}}
  </script>

Data-attribuutit

HTML5 lisäsi spesifikaatioon mahdollisuuden data-attribuuttien määrittelyyn, kts. linkki. Käytännössä elementteihin voi asettaa attribuutteja joiden etuliite on data. Näitä attribuutteja käsitellään datan varastona: ne eivät vaikuta sivun asetteluun tai ulkoasuun, eikä loppukäyttäjä näe niitä.

Data-attribuuttien hyöty tulee esille esimerkiksi elementteihin asetettavan datan identifioinnissa.

Lisätään seuraavaksi sivulle toiminnallisuus, joka näyttää elementin tiedot elementtiä klikattaessa. Lähdetään pienestä liikkeelle, ja näytetään ensin erillisessä ikkunassa klikattuun elementtiin liittyvä tunnus. Voimme tutkia tunnusta JQueryn tarjoaman $(this)-arvon kautta. Uudemmat JQueryn versiot tarjoavat myös komennon data, joka hakee elementtiin liittyvien data-elementtien arvon. Esimerkiksi alla oleva komento ...data("id") hakee elementissä data-id olevan arvon.

function showdata() {
  // ...
  // näytetään tulokset
  $("#resultview").html(html);

  // lisätään tapahtumankäsittely
  $("#resultview p").click(function() {
      alert($(this).data("id"));
  });
}

Henkilön näyttäminen

Toteutetaan henkilön näyttäminen siten, että kun henkilön nimeä klikataan, sovellus näyttää henkilöön liittyvät tiedot resultview alueella, mutta erillisen templaten avulla. Luodaan ensin "detailstemplate"-niminen template henkilön tietojen näyttämiseen. Templatessa näytetään henkilön nimi sekä ikä.

  <script id="detailstemplate" type="text/html">
    <h2>{{name}}</h2>
 
    <p>Age: {{age}}</p>
  </script>

Lisätään koodiin erillinen funktio templaten näyttämiseen. Funktio displayDetails saa parametrina näytettävän henkilön tunnuksen ja näyttää henkilön tiedot tunnuksella "resultview" merkityssä alueessa. Itse ohjelmakoodi hakee ensin henkilön tunnuksella JQueryn grep-komennon avulla. Komento grep palauttaa listan, joten tarkistamme että tuloksia on vain yksi. Tämän jälkeen tulos renderöidään mustachen avulla näkyville.

function displayDetails(id) {
  var resultlist = $.grep(data, function(person, index) {
    return person.id == id;
  });
  
  if(resultlist.length != 1) {
    console.log("Could not find a person with id " + id);
    return;
  }
  
  $("#resultview").html(Mustache.render($("#detailstemplate").html(), resultlist[0]));
}

Huomaamme taas, että sovelluksen rakenne ei ole kovin selkeä. Sovellusta olisi selventänyt huomattavasti erillisen model-luokan luominen ja vastuiden pilkkominen sopivasti osa-alueisiin. Esimerkiksi filtteröinti ja hakeminen olisi kannattanut abstrahoida pois näkymään liittyvistä funktioista.

Mugshots

Tehtäväpohjassa on mukana sovellus, joka lukee tiedoston data.json sisällön ja listaa siinä olevat elementit sivulle näkyväksi. Data kannattanee hakea $.getJSON-kutsulla. Tehtävänäsi on lisätä sivulle toiminnallisuutta.

Kun käyttäjä vie hiiren tekstin päälle, tulee kyseiseen tekstiin liittyvä kuva näyttää tekstien sivulla. Kuvien sijainnit löytyvät tiedostosta mugshots.json. Kummassakin tiedostossa olevassa datassa on id-tunnuskenttä, jonka perusteella voit päätellä mikä kuva tulee näyttää mihinkin tekstiin liittyen.

Alla esimerkki siitä, miltä sovelluksen tulee näyttää kun hiiri viedään tekstin "Yarrr!"-päälle.

Vinkki! Pääset vaikuttamaan elementtien sijaintiin sivulla mm. CSS:n float-attribuutin avulla.

Sovelluksen rakenteen hallinta: Backbone.js

Kasvavan sovelluksen rakenteen hallinta ei ole helppoa. Edellisessä henkilöiden hallintaan liittyvässä esimerkissä rakensimme tarkoituksella pala palalta sovelluksen siten, että siihen lisättiin aina vähän toiminnallisuutta. Sovelluksen rakenne pysyi kutakuinkin ymmärrettävänä, mutta melko nopeasti huomasi, että datan käsittelyn erottamisesta käyttöliittymälogiikasta olisi ollut huomattavasti hyötyä. Pienessä omaan käyttöön tulevassa testisovelluksessa sovelluksen rakenteella tai ulkomuodolla ei ole niin väliä, mutta useamman henkilön kanssa työtä tehdessä sovelluksella tulee olla selkeä rakenne.

Selkeään rakenteeseen kuuluu muunmuassa käyttöliittymän ja sovelluksen datan erottaminen toisistaan. Paljon esillä olevat suunnittelumallit kuten MVC pyrkivät juuri tähän: käyttöliittymän ei tule tarvita tietää datasta, eikä datan käyttöliittymästä. Rakenteeseen kuuluu myös mekanismi viestien kuljettamiseen käyttöliittymältä datalle ja toisinpäin. JavaScriptissä tähän käytetään usein tapahtumia ja Observer-suunnittelumallia.

Tutustutaan seuraavaksi Backbone.js-sovelluskehykseen, joka tarjoaa tuen web-sovellusten rakenteen ylläpitoon. Käytännössä Backbone tarjoaa joukon konstruktori- ja apufunktioita Model ja View-olioiden tekemiseen, sekä niiden väliseen kommunikointiin.

Backbone tarvitsee toimiakseen Underscore.js-kirjaston, jonka lisäksi se hyötyy huomattavasti JQuerystä. Lähdetään luomaan sovellusta kirjojen hallintaan. Sovellus tarjoaa mahdollisuuden kirjojen lisäämiseen ja poistamiseen, sekä kirjojen listaamiseen. Sivun runko on aluksi seuraavanlainen.

<!DOCTYPE html>
<html>
    <head>
        <title>Kirjasto</title>
        <meta charset="UTF-8">
        <!-- tyylitiedostot -->
    </head>
    <body>
        <header>
            <h1>Library</h1>
            <nav>
                <ul>
                    <li><a href="#">Add</a></li>
                    <li><a href="#">List</a></li>
                </ul>
            </nav>
        </header>

        <!-- runko -->
        <section id="main"></section>


        <!-- templatet -->

        <!-- JavaScript-kirjastot -->
        <script src="jquery-1.8.2.min.js"></script>
        <script src="mustache.js"></script>

        <script src="underscore-min.js"></script>
        <script src="backbone-min.js"></script>

        <!-- Omat lähdekooditiedostot -->
    </body>
</html>

Sivun lähdekoodeja varten on luotu myös runko, johon lähdekoodit asetetaan.

var library = {
    model: {},
    view:{}
};

Huom! Kuten ennenkin, kannattaa tehdä esimerkit omalla koneella samalla kun niitä lukee. Tämä auttaa huomattavasti materiaalin ja tehtävien sisäistämisessä!

View

Tutustutaan näkymien (View) luontiin Backbonen avulla. Backbonessa näkymän konstruktorifunktiot luodaan komennolla Backbone.View.extend, jolle annetaan parametrina näkymään liittyviä attribuutteja. Jokainen näkymä liittyy johonkin sivulla olevaan elementtiin (attribuutti el), ja näkymään liittyy usein tapahtumia, joita kutsutaan erilaisten tapahtumien yhteydessä (events). Luodaan ensin näkymä valikolle.

library.view.NavigationView = Backbone.View.extend({
      // mihin elementtiin näkymä liittyy (header)
      el: $("header"),
      // mitä tapahtumia elementissä kuunntellaan, ja mitä funktioita kutsutaan niiden tapahtuessa
      events: {
          // kun ensimmäistä linkkiä klikataan, kutsu funktiota add
          // vrt. $("header a:nth(0)").click(function() { add() });
          "click a:nth(0)": "add",
          // kun toista linkkiä klikataan, kutsu funktiota list
          "click a:nth(1)": "list"
      },
      // funktio add
      add: function(eventInformation) {
          console.log("add clicked");
          eventInformation.preventDefault();
      },
      // ja funktio list
      list: function(eventInformation) {
          console.log("list clicked");
          eventInformation.preventDefault();
      }
  });

Yllä olevassa näkymää varten luodussa koodissa näkymä on elementin header sisällä, ja siihen liittyy kaksi tapahtumankuuntelijaa (events). Jos ensimmäistä linkkiä painetaan, tehdään metodikutsu add. Toista linkkiä painettaessa tehdään metodikutsu list.

Jos haluamme kokeilla ylläolevan näkymän toimintaa, voimme lisätä sen luomiskutsun osaksi JQueryn $(document).ready-kutsua. Alla olevassa esimerkissä näkymä luodaan kun sivu on latautunut, jonka jälkeen linkkien painaminen kirjoittaa viestejä selaimen lokiin.

  $(document).ready(function() {
      new library.view.NavigationView();
  });

Navigaationäkymämme ei oikeastaan tee vielä mitään erityisen mielekästä, joten lisätään sivulle toinen näkymä. Näkymä AddView tarjoaa toiminnallisuuden kirjan tietojen lisäämiseen. Luodaan ensin kirjan lisäämiseen käytettävä template. Template sisältää kentät kirjan nimelle, sivujen määrälle, ja isbn-tunnukselle.

        <script id="add-book-template" type="text/html">
            <h1>Add Book</h1>
            <form>
                <label for="name">Name</label>
                <input type="text" name="name" id="name"/>
                <label for="pages">Pages</label>
                <input type="text" name="pages" id="pages"/>
                <label for="isbn">ISBN</label>
                <input type="text" name="isbn" id="isbn"/>
                <input type="button" value="Add" id="add-book"/>
            </form>
        </script>

Backbonen näkymille voi antaa myös initialize-parametrin, joka kertoo mitä tehdään näkymää luotaessa. Kun näkymä luodaan, haluamme lisätä yllä olevan templaten sisällön sivulla olevaan "main"-tunnuksella merkittyyn elementtiin. Näkymä AddView kuuntelee myös tapahtumia. Jos sen sisällä olevaa tunnuksella "add-book" merkittyä elementtiä klikataan, kutsutaan metodia save. Näkymälle on määritelty myös apufunktio serialize, joka luo käytännössä olion lomakkeessa olevasta datasta.

library.view.AddView = Backbone.View.extend({
    // mihin elementtiin näkymä liittyy (#main)
    el: $("#main"),
    // mitä tehdään kun näkymä luodaan (luodaan lisäysnäkymä)
    initialize: function() {
        $("#main").html($("#add-book-template").html());
    },
    // tapahtumat
    events: {
        // kun #main -alueessa olevaa #add-book elementtiä painetaan, kutsu metodia save
        "click #add-book": "save"
    },
    // apufunktio lomakkeen arvojen käsittelyyn
    serialize: function() {
        return {
            name: $("#name").val(),
            pages: $("#pages").val(),
            isbn: $("#isbn").val()
        }
    },
    // funktio, jota kutsutaan, kun lomakkeen nappia painetaan
    save: function() {
        var data = this.serialize();
        console.log("Serialized: " + JSON.stringify(data));
        // o-noes, we need to do some stuff with this!
    }
});

Muokataan seuraavaksi näkymän NavigationView sisältöä siten, että kun linkkiä Add klikataan, luodaan uusi instanssi AddView-elementistä.

library.view.NavigationView = Backbone.View.extend({
    // ... 
    add: function(eventInformation) {
        new library.view.AddView();
        eventInformation.preventDefault();
    },
    // ...

Nyt linkin "Add" painaminen näyttää lomakkeen lisäämiseen käytettävän näkymän. Kun lomakkeeseen lisätään dataa, ja nappia "Add" painetaan, näkyy konsolissa (esimerkiksi) seuraavanlainen viesti.

Serialized: {"name":"Harry Potter ja viisasten kivi","pages":"335","isbn":"951-31-1146-6"} 

Datalle ei kuitenkaan vielä tehdä mitään.

HelloView

Toteuta tehtäväpohjaan Backbone.js:n avulla näkymä HelloView, joka sisältää laskurin. Kun elementtiä, jossa laskurin teksti on, klikataan, laskurin arvon tulee kasvaa yhdellä. Rakenna näkymä siten, että se käyttää "index.html"-tiedostossa olevaa tunnuksella "main" merkittyä elementtiä näkymän juurielementtinä. Vinkki: voit määritellä näkymään liittyviä muuttujia initialize-metodin yhteydessä this-etuliitteellä.

Luo näkymä kun sivu on ladattu. Alla esimerkkejä näkymän sivulle luomasta tekstistä. Ensimmäisessä kuvassa elementtiä ei ole klikattu kertaakaan, toisessa kuvassa elementtiä on klikattu 4 kertaa.

Kun sivusi toimii halutusti, palauta se TMC:lle.

Model

Jotta voisimme tehdä kirjadatalle jotain, meillä tulee olla siitä erillinen dataesitys. Luodaan ensin näkymästä täysin erillinen malli. Backbone tarjoaa komennon Backbone.Model.extend, jonka avulla voidaan luoda konstruktorifunktioita. Komento saa parametrina joukon attribuutteja, joita käsitellään olion luonnin yhteydessä. Luodaan konstruktorifunktio library.model.Book, jolla on oma validointifunktio.

library.model.Book = Backbone.Model.extend({
    validate: function(attributes) {
        if(!attributes.name || attributes.name.length < 1) {
            return "Invalid name: " + attributes.name;
        }
        
        if(!attributes.pages || attributes.pages.length < 1 || isNaN(Number(attributes.pages))) {
            return "Invalid page count" + attributes.pages;
        }
        
        if(!attributes.isbn || attributes.isbn.length < 1) {
            return "Invalid isbn " + attributes.isbn;
        }
    }
});

Backbonen mallitoiminnallisuus tarjoaa muunmuassa toiminnallisuuden luokan luontiin attribuuttien avulla. Ylläolevasta luokasta voidaan tehdä ilmentymä antamalla sille olio, jossa on halutut attribuutit. Esimerkiksi alla luodaan kirjaolio, jolla on attribuutit name, pages ja isbn. Attribuutteihin pääsee käsiksi komennolla get, ja niitä voi muuttaa komennolla set. Metodi validate on Backbonen malleihin kuuluva valmis metodi, johon pääsee käsiksi helpon aksessorin isValid avulla.

var book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});
console.log("nimi: " + book.get("name"));
console.log("sivuja: " + book.get("pages"));
console.log("isbn: " + book.get("isbn"));

console.log("ok? " + book.isValid());

// jätetään isbn tyhjäksi
book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:""});
console.log("ok? " + book.isValid());
nimi: Harry Potter ja viisasten kivi
sivuja: 335
isbn: 951-31-1146-6
ok? true 
ok? false 

Mielenkiintoista validoinnissa on se, että se estää huonojen tekojen tekemisen. Esimerkiksi isbn-numeron asettaminen validista versiosta epävalidiksi ei onnistu. Näemme tämän helposti mallista esille saatavan JSON-esityksen avulla. Tuttu JSON.parse ei valitettavasti toimi Backbonen olioilla, mutta komento toJSON auttaa.

var book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});
console.log("json-muotoisena: " + JSON.stringify(book.toJSON()));
book.set("isbn", "");
console.log("json-muotoisena kun ISBN on yritetty asettaa: " + JSON.stringify(book.toJSON()));
json-muotoisena: {"name":"Harry Potter ja viisasten kivi","pages":"335","isbn":"951-31-1146-6"}
json-muotoisena: {"name":"Harry Potter ja viisasten kivi","pages":"335","isbn":"951-31-1146-6"} 

Olioita on mahdollista luoda myös antamalla konstruktorifunktiolle parametreina yksittäisiä arvoja. Määrittelemällä initialize-metodin oliolle voi lisätä arvoja yksi kerrallaan. Alla on täydennetty ylläolevaa esimerkkiä siten, että sillä on validoinnin lisäksi komento initialize.

library.model.Book = Backbone.Model.extend({
    initialize: function(name, pages, isbn) {
        this.set({
            name: name,
            pages: pages,
            isbn: isbn
        });
    },
    validate: function(attributes) {
        if(!attributes.name || attributes.name.length < 1) {
            return "Invalid name: " + attributes.name;
        }
        
        if(!attributes.pages || attributes.pages.length < 1 || isNaN(Number(attributes.pages))) {
            return "Invalid page count" + attributes.pages;
        }
        
        if(!attributes.isbn || attributes.isbn.length < 1) {
            return "Invalid isbn " + attributes.isbn;
        }
    }
});

Nyt olioiden luominen onnistuu seuraavasti:

var book = new library.model.Book("Harry Potter ja viisasten kivi", "335", "951-31-1146-6");
console.log("json-muotoisena: " + JSON.stringify(book.toJSON()));
json-muotoisena: {"name":"Harry Potter ja viisasten kivi","pages":"335","isbn":"951-31-1146-6"}

Huom! Funktion initialize määrittely muuttaa konstuktorin toimintaa. Jos yllä määritellystä kirjasta luodaan uusi olio kutsulla new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});, asetetaan kirjan nimeksi parametrina annettu olio. Oletetaan jatkossa, että kirjaoliolla ei ole erillistä initialize-funktiota.

HelloModel

Toteuta tehtäväpohjaan Backbone.js:n avulla malli Person, joka sisältää attribuutit "name", "age" ja "abilities". Mallin tulee olla "muuttujan" site.model sisällä, ja sen attribuutin "abilities" tulee olla lista. Toteuta malli siten, että sen voi luoda seuraavasti:

var mikke = new site.model.Person("mikke", 28, ["Coding", "Music", "Reproduction"]);

Lisää tehtäväpohjassa valmiina olevalle näkymälle tapahtuma: kun elementtiä, johon malli on renderöity, klikataan, mallin iän arvon tulee kasvaa yhdellä (attribuutin age arvon tulee kasvaa yhdellä), jonka jälkeen näkymä renderöidään uudestaan.

Luo näkymä kun sivu on ladattu. Alla on esimerkkejä näkymän sivulle luomasta tekstistä. Ensimmäisessä kuvassa elementtiä ei ole klikattu kertaakaan, toisessa kuvassa elementtiä on klikattu 4 kertaa.

Kun sivusi toimii halutusti, palauta se TMC:lle.

Modelin ja Viewn liittäminen

Kun käytössämme on malli, voimme lisätä sen osaksi sivua. Mallin lisääminen tapahtuu antamalla se parametrina näkymälle. Muokataan sivun luontikutsua siten, että näkymämme NavigationView saa konstruktorikutsun yhteydessä parametrina Book-olion.

$(document).ready(function() {
    var book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});
    
    new library.view.NavigationView({
        model: book
    });
});

Luodaan ensimmäinen versio ListView-oliosta. ListView-olion ensimmäinen versio näyttää yksittäisen kirjan, ja sen käyttämä HTML-template on seuraavanlainen.

        <script id="single-book-template" type="text/html">
            <p>
                <label for="name">Name</label>
                <span name="name">{{name}}</span><br/>
                <label for="pages">Pages</label>
                <span name="pages">{{pages}}</span><br/>
                <label for="isbn">ISBN</label>
                <span name="isbn">{{isbn}}</span>
            </p>
        </script>

Itse ListView näyttää seuraavanlaiselta. Listanäkymään liittyy this.model-olio, jonka se saa parametrina konstruktorikutsun yhteydessä. Näkymän toiminnallisuus on yksinkertainen: se näyttää siihen liittyvän mallin sisällön HTML-sivulla.

library.view.ListView = Backbone.View.extend({
    el: $("#main"),
    initialize: function() {
        var html = Mustache.render($("#single-book-template").html(), this.model.toJSON());
        $("#main").html(html);
    }
});

Muokataan seuraavaksi NavigationView-näkymää siten, että se luo ListView-olion kun list-metodia kutsutaan. NavigationView antaa ListView-näkymän konstruktorille parametriksi sen itsensä sisältämän model-olion, jonka se saa omassa konstruktorikutsussaan.

library.view.NavigationView = Backbone.View.extend({
    // ...
    list: function(eventInformation) {
        new library.view.ListView({
            model: this.model
        });
    }
    // ...

Nyt näkymä ListView näyttää aina siihen liittyvän kirja-olion tiedot listaa klikattaessa. Sama muutos tulee tehdä myös AddView-näkymän luomiseen.

library.view.NavigationView = Backbone.View.extend({
    // ... 
    add: function(eventInformation) {
        new library.view.AddView({
            model: this.model
        });
        eventInformation.preventDefault();
    },
    // ...

Entä jos kirjan tiedot muuttuvat model-oliossa?

Jos kirjan tiedot muuttuvat model-oliossa, tulee näkymän myös päivittyä. Modeliin voi lisätä tapahtumankuuntelijoita komennolla bind. Erilaisten tapahtumien lista löytyy osoitteesta http://backbonejs.org/#FAQ-events. Haluamme kuunnella model-oliossa tapahtuvia muutoksia. Aina kun olio muuttuu, tulee meidän näyttää näkymä uudestaan.

Aiemmassa ListView-versiossa näkymän näyttäminen tapahtui osana initialize-kutsua. Siirretään näkymän näyttäminen erilliselle render-metodille, ja lisätään initialize-kutsuun modelin muutosten kuuntelu. Komennolla this.model.bind liitämme tähän näkymään liittyvään malliin parametrina annetun tyyppisen tapahtuman kuuntelijan, joka kutsuu parametrina annettavaa metodia parametrina annetulta oliolta. (huh!)

library.view.ListView = Backbone.View.extend({
    el: $("#main"),
    initialize: function() {
        this.model.bind("change", this.render, this);
        this.render(); // näytetään näkymä olion luonnin yhteydessä
    },
    render: function() {
        var html = Mustache.render($("#single-book-template").html(), this.model.toJSON());
        $("#main").html(html);
    }                
});

Näkymän päivittymistä voi testata lisäämällä automaattisen päivitysskriptan osaksi sivun latautumista (muista ottaa se myös pois!). Javascriptin komento setInterval saa parametrina funktion ja luvun. Kun sitä kutsutaan, se alkaa kutsumaan parametria kerran luvun (millisekunteja) määräämässä ajassa. Alla päivitämme mallikirjan nimeä kerran viidessä sekunnissa.

$(document).ready(function() {
    var book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});
    
    new library.view.NavigationView({
        model: book
    });
    
    setInterval(function() {
        book.set("name", book.get("name") + " :)");
    }, 5000);
});

Nyt "Harry Potter ja viisasten kivi" paranee silmissä!

ModelAction

Tehtäväpohjassa on mukana Backbonella luotu sovellus, joka sisältää modelin Clock ja näkymän ClockView. Sovelluksen käynnistyessä tapahtuu kutsu setInterval, jonka avulla kelloon asetettua aikaa päivitetään joka sekunti yhdellä. Tehtävänäsi on muokata sovellusta siten, että kun mallissa oleva data muuttuu, näkymän tulee renderöidä itsensä uudestaan. Alla on esimerkkejä näkymän sivulle luomasta tekstistä. Ensimmäisessä kuvassa sivun lataamisesta on kulunut noin 7 sekuntia, toisessa noin 265 sekuntia.

Kun sivusi toimii halutusti, palauta se TMC:lle.

Collections

Kirjastosovelluksemme ei ole vielä mielekäs, sillä se sisältää vain yhden kirjan. Jotta kirjastomme sisältäisi useampia kirjoja, tulee käytössämme olla jonkinlainen kokoelma. Backbone tarjoaa kahdenlaisia malleja: Model kuvaa yksittäistä asiaa, ja Collection kuvaa joukkoa yksittäisiä asioita. Tehdään kokoelmakonstruktori BookList, joka sisältää kirjoja. Kokoelman sisältämä mallityyppi annetaan parametrilla model.

library.model.BookList = Backbone.Collection.extend({
    model: library.model.Book
});

Kokoelmaan voi lisätä uusia kirjoja add-metodilla. Lisätään seuraavaksi toiminnallisuus listan näyttämiseen. Muokataan ensin sivun lataustoiminnallisuutta siten, että navigaationäkymä saa parametrina books-kokoelman.

$(document).ready(function() {
    var book = new library.model.Book({name:"Harry Potter ja viisasten kivi",pages:"335",isbn:"951-31-1146-6"});
    var books = new library.model.BookList();
    books.add(book);
    
    new library.view.NavigationView({
        model: books
    });
});

Muokataan tämän jälkeen listanäkymää. Lisätään ensin HTML-template, joka listaa kaikki kirjat. Jos kirjoja ei ole ollenkaan, näytetään viesti "No books :(".

        <script id="list-books-template" type="text/html">
            {{#list}}
            <p>
                <label for="name">Name</label>
                <span name="name">{{name}}</span><br/>
                <label for="pages">Pages</label>
                <span name="pages">{{pages}}</span><br/>
                <label for="isbn">ISBN</label>
                <span name="isbn">{{isbn}}</span>
            </p>
            {{/list}}
            {{^list}}
            <p>No books :(</p>
            {{/list}}
        </script>

Muokataan ListView-näkymää siten, että se käyttää tunnuksella "list-books-template" merkattua templatea. Koska käytämme Mustachea datan renderöintiin, luodaan dataa varten erillinen olio, jossa olevassa attribuutissa "list" kirjat ovat.

library.view.ListView = Backbone.View.extend({
    // konteksti
    el: $("#main"),
    initialize: function() {
        this.model.bind("change", this.render, this);
        this.render();
    },
    render: function() {
        var data = {
            list: this.model.toJSON()
        };
        
        var html = Mustache.render($("#list-books-template").html(), data);
        $("#main").html(html);
    }                
});

Nyt näemme kirjat listana. Jos lisäämme sovellukseen aiemmin kokeilemamme setInterval-kutsun, huomaamme, että näkymä päivittyy vieläkin. Kokoelman sisällä tapahtuvat tapahtumat näkyvät siis myös kokoelman ulkopuolelle.

Muokataan seuraavaksi näkymää AddView siten, että lisää kirjan kokoelmaan. Kokoelmaan liittyvä add hoitaa myös datan validoinnin. Se heittää poikkeuksen jos näkymään lisättävässä datassa on virhe. Poikkeusten käsittely tapahtuu Javasta tutulla try-catch-syntaksilla. Lisätään AddView-näkymään toiminnallisuus, jossa käyttäjälle näytetään virhe jos lisääminen epäonnistuu. Jos lisääminen onnistuu, näytetään teksti "Kiitsä!".

library.view.AddView = Backbone.View.extend({
    // ...
    save: function() {
        var data = this.serialize();
        try {
            this.model.add(data);
        } catch (err) {
            alert(err);
            return;
        }
        
        $("#main").html("<h2>Kiitsä!</h2>");
    }
    // ...

Datan lisääminen kirjaoliona kokoelmaan onnistuu myös.

library.view.AddView = Backbone.View.extend({
    // ...
    save: function() {
        var data = this.serialize();
        try {
            this.model.add( new library.model.Book(data) );
        } catch (err) {
            alert(err);
            return;
        }
        
        $("#main").html("<h2>Kiitsä!</h2>");
    }
    // ...

Huom! Tutki mitä tapahtuu kun yrität lisätä kirjaa, joka ei ole validi. Jos modelilla on validointi, ja validointi epäonnistuu, tulee uusi kirja olemaan tyhjä. Kokeile samaa myös seuraavalla koodilla, huomaatko toiminnallisuudessa mitään eroja?

library.view.AddView = Backbone.View.extend({
    // ...
    save: function() {
        var data = this.serialize();
        var book = new library.model.Book(data);
        try {
            this.model.add( book );
        } catch (err) {
            alert(err);
            return;
        }
        
        $("#main").html("<h2>Kiitsä!</h2>");
    }
    // ...

Muokataan lopulta AddView-näkymän initialize-funktiota siten, että se poistaa alustusvaiheessa "click"-tapahtumankäsittelijän. Tämä estää sen, ettei samalla näkymällä ole useita klikkauksenkuuntelijoita.

library.view.AddView = Backbone.View.extend({
    // ...
    initialize: function() {
        $("#main").unbind("click");
        $("#main").html($("#add-book-template").html());
    },
    // ...

Sleepy? (3p)

Tässä tehtävässä sinun tulee toteuttaa unipäiväkirja. Unipäiväkirjassa on kaksi toimintoa, unen lisääminen ja unistatistiikan listaaminen. Jokaisesta nukkumisesta kerätään alkukellonaika, loppukellonaika, ja freesiys (1-5). Pääsivu näyttää seuraavalta:

Kun pääsivulta valitsee vaihtoehdon "Add entry", käyttäjälle näytetään seuraavanlainen näkymä.

Näkymän voi täyttää normaalisti. Näkymän tulee tarkistaa, että freesiys-kenttään asetetaan numero, joka on väliltä 1-5.

Kun unipäiväkirjaan lisätään tietue, käyttäjälle näytetään viesti "Thx!".

Kun käyttäjä valitsee vaihtoehdon "Statistics", käyttäjälle näytetään näkymä, joka listaa tietueet. Näkymän lopussa tulostetaan freesiys-arvo, joka on keskiarvo kaikkien tietueiden sisältämistä arvoista.

Huom! Toteuta tehtävä Backbonea käyttäen!

Vinkki! Voit iteroida kokoelmassa olevia yksittäisiä malli-olioita JQueryn .each-komennon avulla. Kun olet valmis ja sivusi toimii kuten pitää, palauta se TMC:lle.

Kommunikointi palvelimen kanssa

Backbone tarjoaa tuen myös palvelimen kanssa kommunikointiin. Jos Collection-oliolle lisätään attribuutti url, osaa backbone hakea dataa palvelimelta. Datan haku onnistuu esimerkiksi collection-oliolle liittyvällä fetch kutsulla, jolle voi antaa parametrina datan lisääminen kokoelmaan {add : true}. Luodaan kaksi konstruktoria. Toista käytetään model-olioiden luontiin, ja toista kokoelmien luontiin.

site.model.Cool = Backbone.Model.extend({
    initialize: function() {
        this.save();
    }
});

site.model.Coollection = Backbone.Collection.extend({
    url: "http://rest-apin-osoite.com/cools",
    model: site.model.Cool
});

Kun luomme kokoelmaolion, ja kutsumme sen fetch-metodia, kokoelma hakee kaikki siihen liittyvät datat.

var collection = new site.model.Coollection();
collection.fetch({ add: true });

Koska model-oliolla on initialize-funktiossa metodikutsu save, se tallennetaan automaattisesti kokoelmassa määriteltyyn osoitteeseen. Huom! Automaattinen tallennus onnistuu koska luomme uuden Cool-olion Coollection-olion kautta. Huomaa myös, että kun komento fetch lisää haettuja olioita kokoelmaan, lisäystapahtuman tyyppi on "add". Jos haluat, että näkymässä tapahtuu muutos, niin kytkös tapahtumatyyppiin "change" ei siis riitä.

var collection = new site.model.Coollection();
collection.fetch({ add: true });

collection.add({coolness:42});

Backbone.js olettaa, että rajapinta on REST-tyylinen.

Tasks (3p)

Toteuta sovellus tehtävien hallintaan. Kunkin tehtävän tulee sisältää tieto id eli tunnus, title eli otsikko, ja completed eli onko tehtävä tehty. Tehtäviä hallinnoivan kokoelmaolion tulee hakea tehtävät palvelimelta osoitteesta http://pure-escarpment-3768.herokuapp.com/app/tasks. Sovelluksesi tulee pystyä myös lisäämään ja muokkaamaan tehtäviä.

Kun sovellus käynnistyy, sen tulee hakea olemassaolevat tehtävät. Alla olevassa kuvassa tehtäviä on 4, ja yhtäkään niistä ei ole merkattu tehdyksi.

Toteuta sovellukseen toiminnallisuus, jonka avulla tehtävän tila (completed) muuttuu sitä klikattaessa. Kun tehtävää klikataan, tilan tulee päivittyä myös palvelimelle. Alla kaksi tehtävää on merkattu tehdyiksi. Huom! Tehtäviä tulee pystyä merkitsemään myös tekemättömiksi.

Tehtävien lisäyksen tulee tapahtua tekstikentän kautta.

Tehtävä lisätään, kun käyttäjä painaa enteriä. (keycode 13)

Huom! Toteuta tehtävä Backbonea käyttäen!

Kun sovelluksesi toimii kuten toivottu, lähetä se TMC:lle.

Reititys

Backbone.js tarjoaa myös historiatuen, jossa sivujen välillä tapahtuva navigointi tapahtuu ankkuritageja käyttäen. Ankkuritagien avulla yhden sivun saa kuuntelemaan esimerkiksi osoitetta sivu.html#add ja toisen sivun kuuntelemaan osoitetta sivu.html#remove. Tämä mahdollistaa sivujen lisäämisen kirjanmerkkeihin, sekä helpottaa esimerkiksi sivujen läpikäyntiä erilaisten crawlerien toimesta (ml. google).

Lisää reitittimistä löytyy osoitteesta http://backbonejs.org/#Router

Responsive Web Design

Responsive Web Design (RWD) on web-sivustojen suunnittelutapa, missä sivut rakennetaan siten, että mukautuvat eri laitteille. Selainlaitteen ominaisuuksiin reagoivien sivujen tarve kasvaa jatkuvasta, sillä web-sivustoja näyttävien laitteiden määrä kasvaa myös jatkuvasti.

Käytännössä laitteen ominaisuuksiin mukautuva sivu vaatii kolme osaa toimiakseen: (1) mukautuvan asettelun, (2) mukautuvan median (kuvat), ja (3) mediakyselyt. Ennen kuin tutustumme näihin tarkemmin, käy Helsingin sanomien sivuilla (http://www.hs.fi/), ja kokeile mitä tapahtuu kun pienennät ja suurennat ikkunaa leveyssuunnassa. Käy tämän jälkeen tutustumassa osoitteessa http://twitter.github.com/bootstrap/examples/hero.html olevaan sivuun ja pienennä ja suurenna sitä sitä leveyssuunnassa. Tutustu myös osoitteessa http://earthhour.fr/ olevaan sivuun.

Mitkä edellämainituista reagoivat tekemiisi muutoksiin?

Twitter Bootstrap

Twitter Bootstrap on kirjasto, joka tarjoaa tuen reagoivien sivujen tekemiseen.

Flickr Bootstrap? (2p)

Tutustu osoitteessa http://twitter.github.com/bootstrap/examples/carousel.html olevaan sivuun. Tehtävänäsi on muokata sivua siten, että karusellielementin kuvat haetaan Flickr-kuvapalvelusta.

Sivu on kopioitu tehtäväpohjan mukana tulevaan index.html-tiedostoon.

Huom! Pääset Carousel-esimerkkiin käsiksi myös githubin kautta.

Muutamia hyödyllisiä linkkejä:

Kun sivu lataa kuvat haluamastasi Flickr-lähteestä, palauta tehtävä TMC:lle. Alla esimerkkikuva, missä kuvat on noudettu teemalla "Mojito".

Luettavaa

Tutustu osoitteessa http://www.alistapart.com/articles/responsive-web-design/ olevaan artikkeliin ennen jatkamista. Ethan Marcotten kirjoittama artikkeli on (lähes) ensimmäinen Responsive Web Design-lähestymistapaa käsittelevä artikkeli, ja selittää vieläkin oleellisimmat asiat erittäin hyvin.

Mukautuva asettelu

Mukautuvalla asettelulla tarkoitetaan yleensä, että sivu rakennetaan osista, joita voidaan asettaa sekä vaaka- että pystysuuntaan, ja jotka voivat venyä siten, että ne täyttävät tyhjän tilan. Osia voi laittaa myös toistensa sisään. CSS3 tarjoaa tähän (vielä keskeneräisen) FlexBox-spesifikaation. FlexBox-spesifikaatio on vielä melko keskeneräinen, ja se on muuttunut matkansa varrella. Sivusto Can I use... tarjoaa hyvän katsauksen selainten tukemiin ominaisuuksiin, esimerkiksi selainten flexbox-tuki löytyy sivulla olevasta linkistä "Flexible Box Layout Module". Esimerkiksi firefoxin flexbox-tuki on tällä hetkellä vaillinainen.

Flexbox-asettelu tapahtuu halutulle alueelle määrätylle display-tyylille flex. Tämä tarkoittaa, että elementin sisällä olevia elementtejä saa venyttää jos näytöllä on tilaa, ja niille on määritelty venyvä tyyli. Elementti, jolla on attribuutin flex arvona 1, saa venyttää (Huom! attribuutti tarvitsee toistaiseksi selaimiin liittyvät etuliitteet.). Tutkitaan kahta esimerkkiä, jotka käyttävät seuraavaa tyylitiedostoa.

#aseteltu {
    display: -moz-flex;
    display: -webkit-flex;
    display: flex;
}

.solu {
    background: #eee;
    padding: 30px;
}

.venyva {
    -moz-flex: 1;
    -webkit-flex: 1;
    flex: 1;
}

.paikallaan {
    background: #ccc;
    width: 100px;
}
<!-- ... -->
  <section id="aseteltu">
    <article class="solu paikallaan">
  
      <p>Tämä solu on fiksattu paikalleen.</p>	
      
    </article>
    <article class="solu venyva">

      <p>Tämä saa venyä niin paljon kuin haluaa.</p>	

    </article>
    <article class="solu paikallaan">

      <p>Tämä solu on fiksattu paikalleen.</p>	
      
    </article>
  </section>
<!-- ..  -->

Yllä oleva käyttöliittymä luo seuraavanlaisen näkymän.

Tämä solu on fiksattu paikalleen.

Tämä saa venyä niin paljon kuin haluaa.

Tämä solu on fiksattu paikalleen.

Vastaavasti, jos määrätyn kokoinen solu on keskellä, ja venyvät solut sivuilla, jakaa selain vapaana olevan tilan sivuilla oleville soluille.

Tämä saa venyä niin paljon kuin haluaa.

Tämä solu on fiksattu paikalleen.

Tämä saa venyä niin paljon kuin haluaa.

Jos (ja kun) etsit lisätietoa mukautuvasta asettelusta netistä, kannattaa huomata, että spesifikaatio elää. CSS-tyylit, joissa käytetään display-attribuutin arvoa box tai flexbox ovat vanhentuneita. Tällä hetkellä oikea arvo on flex. Tarvitset spesifikaatiota varten myös flexboxia tukevan selaimen (esim. uusi Chrome) sekä selainspesifin etuliitteen (esim. -webkit-).

Flexbox mahdollistaa myös elementtien asettelun korkeussuunnassa. Voimme asettaa asettelusuunnan muuttamalla attribuutin flex-direction arvoa. Esimerkiksi alla attribuutin flex-direction arvoksi on asetettu column, joka tarkoittaa, että elementit asetellaan pystysuunnassa.

/* muut asetukset */

#aseteltu {
    display: -moz-flex;
    display: -webkit-flex;
    display: flex;

    -moz-flex-direction: column;
    -webkit-flex-direction: column;
    flex-direction: column;
}

.solu {
    background: #eee;
    padding: 20px;
}

.venyva {
    -moz-flex: 1;
    -webkit-flex: 1;
    flex: 1;
}

.paikallaan {
    background: #ccc;
    height: 50px;
}

Alla olevassa naytossa alueen korkeudeksi on asetettu 300 pikseliä. Kuten huomaat, sarakkeet eivät tällä hetkellä veny korkeussuunnassa flexboxin avulla.

Tämä saa venyä niin paljon kuin haluaa.

Tämä solu on fiksattu paikalleen.

Tämä saa venyä niin paljon kuin haluaa.

Longcat

Tehtäväpohjassa olevassa img kansiossa on kuvat Longcat-kissan päästä, vartalosta, ja jaloista. Tehtävänäsi on luoda näiden avulla sivu, jossa longcat pienenee ja suurenee leveyssuunnassa ikkunaa pienennettäessä ja suurennettaessa. Alla esimerkkejä:

Huom! Vain osa selaimista tukee aiemmin esitettyä flexbox-standardia. Voit palauttaa tehtävän kunhan se toimii uusimmassa Google Chromessa!! Vartaloa rakennettaessa kannattaa käyttää taustakuvilla olevaa kuvan toistotoiminnallisuutta hyödyksi. Kun sivusi toimii kuten toivottu, palauta tehtävä TMC:lle.

Eroon pikseleistä!

Mukautuvassa asettelussa pyritään myös lopettamaan kovakoodattujen pikseliarvojen käyttäminen. Aiemmissa esimerkeissä padding-attribuutin arvoksi asetettiin tietty pikselimäärä. Tämä pikselimäärä ei muutu näytön koon mukaan, vaan pysyy aina samana. Sen sijaan, että käytämme kovakoodattuja pikseliarvoja, haluamme käyttää prosentuaalisia arvoja niin paljon kuin mahdollista. Tutkitaan seuraavaa tyylitiedostoa.

body {
    width: 770px;
}

#aseteltu {
    display: -moz-flex;
    display: -webkit-flex;
    display: flex;
}

.solu {
    background: #eee;
    padding: 30px;
}

.venyva {
    -moz-flex: 1;
    -webkit-flex: 1;
    flex: 1;
}

.paikallaan {
    background: #ccc;
    width: 100px;
}

Tyylitiedostossa on määrätty sivun leveydeksi 770 pikseliä. Tämä ei skaalaudu oikeastaan millekään laitteelle, joten ensimmäinen askel on muokata se prosentuaaliseksi arvoksi. Käytettävä prosentuaalinen riippuu vahvasti halutusta tyylistä, ja käytännössä käyttöliittymäsuunnittelija joutuu kokeilemaan useita arvoja. Oletetaan, että käyttöliittymäsuunnittelija toteaa, että saamme vaihtaa arvon 770 pikseliä 80 prosenttiin selaimen leveydestä.

body {
    width: 80%;
}
/* ... */

Nyt voimme muokata muitakin sivun alueita siten, että ne skaalautuvat näytön leveyden mukaan. Solulle on määritelty attribuutin padding arvoksi 30 pikseliä. Muutetaan sitä siten, että sen arvo ilmaistaan prosenteissa. Koska aiempi sivumme koko oli 770 pikseliä, voimme laskea solun padding-arvon sen kautta. Uusi arvo tulee olemaan noin 4% (30 / 770 = ~0.039).

/* ... */
.solu {
    background: #eee;
    padding: 4%; /* 30 / 770 */
}
/* ... */

Muokataan vielä paikallaan olevien elementtien leveyttä. Paikallaan olevien elementtien leveydeksi oli määritelty 100 pikseliä. Voimme toistaa saman laskun kuin aiemminkin, ja määritellä sen avulla prosentuaalisen leveyden. Uusi leveys tulee olemaan noin 13% (100 / 770 = ~0.13).

/* ... */
.paikallaan {
    background: #ccc;
    width: 13%; /* 100 / 770 */
}
/* ... */

Nyt sivulla oleva "design" mukautuu myös ikkunan koon mukaan.

Eroon pikseleistä fonteissa!

Sama pätee myös fontteihin. Jos vain mahdollista, määritellään fonttien koot joko prosentuaalisina arvoina tai em-arvoina. Tutkitaan esimerkkiä, jossa tekstin tyyli on määritelty tarkkaan pikseleinä.

.teksti {
    font-size: 18px;
    height: 40px;
    line-height: 30px;
}

Yllä fontin kooksi on asetettu 18 pikseliä, koko alueen korkeudeksi 40 pikseliä, ja viivan korkeudeksi 30 pikseliä. Yllä oleva esimerkki ei skaalaudu, joten joudumme korjaamaan sitä hieman. Selainten oletusfonttikoko on 16 pikseliä, joka on lähes kaikissa selaimissa sama kuin 1 em. Muokataan ylläolevaa esimerkkiä siten, että käytämme em-arvoja. Asetetaan fontin kooksi 1.125em (18 / 16 = 1.125), alueen korkeudeksi 2.5em (40 / 16 = 2.5), ja viivan korkeudeksi 1.875em (30 / 16).

.teksti {
    font-size: 1.125em; /* 18 / 16 */
    height: 2.5em; /* 40 / 16 */
    line-height: 1.875em; /* 30 / 16 */
}

Nyt myös fonttimme skaalautuvat tarvittaessa.

Skaalautuvat kuvat ja media

Kun sivuilla käytetään flexboxia sivun asettelun mukauttamiseen, yhdeksi ongelmaksi voi muodostua kuvien ja median koko. Jos sivu, jonka tarkoitus on muokkautua laitteen resoluution mukaan, sisältää ennalta määrätyn kokoisia kuvia, kuvat rajoittavat sivun skaalautumista huomattavasti.

Ensimmäinen tapa rajoittaa kuvien kokoa on skaalata niitä niille tarjolla olevan alueen mukaan automaattisesti. Asettamalla kuville prosentuaalisen maksimikoon, kuva näytetään korkeintaan sen kapseloiman elementin kokoisena. CSS-atribuutti max-width toimii myös useille muille elementeille, joilla on määrätty koko, kuten videolle. Tutkitaan tätä konkreettisesti kahden esimerkin kautta. Ensimmäisessä esimerkissä kuvalle ei ole asetettu max-width-määrettä, joten se ei välitä omasta sijainnistaan dokumentissa.

Tämä saa venyä niin paljon kuin haluaa.

Tämä solu on fiksattu paikalleen.

Tämä saa venyä niin paljon kuin haluaa.

Seuraavaksi lisätään kuvalle attribuutti max-width. Kuva saa käyttää korkeintaan 100% sen sisältävän elementin tilasta.

img {
    max-width: 100%;
}

Tämä saa venyä niin paljon kuin haluaa.

Tämä solu on fiksattu paikalleen.

Tämä saa venyä niin paljon kuin haluaa.

Myös minimikoon voi asettaa. Attribuutti min-width kertoo elementin minimileveyden. Esimerkiksi sivun, jonka leveydeksi haluaa vähintään 450 pikseliä, mutta jos mahdollista, niin 90% näytöstä, voi määritellä seuraavasti.

body {
    min-width: 450px;
    width: 90%;
}

Scalable

Tehtäväpohjassa tulee mukana tuttu MOOC-pohja. Muokkaa sen tyylitiedostoa siten, että sivun leveys on aina vähintään 600 pikseliä, ja muuten 80% käytössä olevasta tilasta. Muuta tämän tyylitiedostossa määriteltyjä attribuutteja siten, että hankkiudut eroon pikseleinä määritellyistä arvoista. Muokkaa lopuksi sivulla käytettyjen kuvien määrittelyä siten, että ne mukautuvat saatavilla olevaan tilaan.

Alla on kuvia sivusta eri kokoisilla selainasetuksilla.

Kun olet valmis, palauta tehtävä TMC:lle.

Mediakyselyt

Yksi ehkäpä CSS3:n hienoimmista uutuuksista on mediakyselyt, joiden avulla tyylitiedostossa voi valita käytettävän tyylin laitteesta riippuen. Käytännössä mediakyselyt mahdollistavat ehdollisten lauseiden kirjoittamisen, esimerkiksi "jos ruudun leveys on korkeintaan 400 pikseliä, käytä flex-orientation-attribuutille arvoa column". Tämä mahdollistaa elementtien erilaisen asettelun riippuen näytön koosta.

Mediakyselyt kirjoitetaan tyylitiedostoon, ja aloitetaan sanalla @media. Tätä seuraa mahdollisesti mediatyyppi, esimerkiksi "screen" tai "print". Jos mediatyyppiä ei määritellä, ei tyyliä rajoitetan vain tietyille mediatyypeille. Tätä seuraa kyselyehdot, jotka erotellaan toisistaan ja/tai -ehdoilla. Jokainen mediakysely sisältää lopulta joukon tyylejä, joita käytetään vain jos mediakyselyyn asetettu ehto pätee. Alla olevassa esimerkissä on täydennetty aiempaa esimerkkiä siten, että elementin "aseteltu" tyyliä täydennetään asettamalla flex-orientation-attribuutin arvoksi column jos ja vain jos ruudun leveys on korkeintaan 400 pikseliä.

#aseteltu {
    display: -moz-flex;
    display: -webkit-flex;
    display: flex;
}

@media (max-width: 400px) {
    #aseteltu {
        -moz-flex-direction: column;
        -webkit-flex-direction: column;
        flex-direction: column;
    }
}

/* ... */

Kyselyitä voi käyttää myös esimerkiksi taustakuvien tai taustavärin valintaan. Vahvasti kuviin pohjautuvissa sivustoissa ei yleensä haluta käyttää samoja kuvia kaikille laitteille. Alla olevassa esimerkissä taustaväri muuttuu jos ikkunan leveys on yli 800 pikseliä -- taustakuvia ym. voisi muuttaa vastaavasti.

body {
    background-color: red;
}

@media (min-width: 800px) {
    body {
        background-color: green;
    }
}

/* ... */

Tämän taustaväri muuttuu kun muutat ikkunan kokoa.

Yksittäisen mediakyselyn sisälle voi määritellä myös useampia tyylejä.

MegaSnake

MegaSnake on mullistava konsepti muotoaan muuttavasta peli. Pienillä käyttöliittymillä pelaajan on tarkoitus päästä pelaamaan megaman-peliä, ja suuremmilla näytöillä matopeliä. Itse pelin toteutus on vielä kesken, eli sinun ei tarvitse välittää siitä. Tehtävänäsi on muokata sivua siten, että jos ruudun leveys on alle 600 pikseliä, käyttäjälle näytetään megaman-kuva alkuperäisen snake-kuvan sijaan. Kuvat löytyvät tehtäväpohjan kansiosta img. Alla on esimerkkejä siitä, miltä sivun tulee näyttää eri kokoisilla selainikkunoilla.

Kun olet valmis, palauta tehtävä TMC:lle.

Vanhempi laitekanta

Mukautuvat web-sivut tarvitsevat tuen laitteen ominaisuuksien kyselyille. Jos sivustoa käyttävä laite ei tue mediakyselyjä, voidaan sivut silti toteuttaa siten, että ne mukautuvat laitteen toiveisiin. Lähes jokainen laite lähettää HTTP-kyselyn yhteydessä tiedon laitteesta osana HTTP-kyselyn otsakkeita. Näiden otsaketietojen avulla laitteesta voi hakea tarkempia tietoja erilaisista laitetietokannoista (esimerkiksi WURFL).

Kun laitteen ruudun leveys on selvillä, voidaan sivun käyttämä tyylitiedoston valita dynaamisesti palvelinpuolella.

Tapahtumat osana dokumenttia

Olemme aiemmin huomanneet, että sivulla oleviin elementteihin voi kytkeä tapahtumankäsittelijöitä. Esimerkiksi tehtävässä "WorldClock" rakennettiin sovellus, joka näytti jokaisella näppäimen painalluksella tietoa mahdollisista paikoista. Kaikki tapahtumankäsittelymme on tähän mennessä liittynyt aina johonkin sivulla olevaan elementtiin. Riippuen sovelluksesta, tämä ei ole aina toivottua. Esimerkiksi peleissä käyttäjän syötteeseen reagoinnin ei tule edellyttää sitä, että tietty sivulla oleva syötekenttä on valittu.

Tapahtumien kuuntelun voi kytkeä myös osaksi dokumenttia. Alla dokumenttiin on kytketty tapahtuma "onkeyup", eli "kun painettu nappi nousee ylös", joka kutsuu aina funktiota "tulosta". Funktiossa "tulosta" näytetään painettuun nappiin liittyvä numeerinen koodi.

function tulosta(eventInformation) {
    alert(eventInformation.which + " " + eventInformation.keyCode);
} 

document.onkeyup = tulosta;

Saman voi tehdä myös JQueryllä. JQueryn hyöty on se, että se normalisoi mm. painetun napin. Painettuun nappiin liittyvä koodi löytyy aina tapahtumaan liittyvästä muuttujasta which.

function tulosta(eventInformation) {
    alert(eventInformation.which);
} 

$(document).ready(function() {
    $(document).keyup(function(eventInformation) {
        tulosta(eventInformation);
    });
});

Batman

Jatketaan hieman ohjelmointiharjoittelua. Tehtävänäsi on täydentää tehtäväpohjassa olevaa sivua siten, että kun käyttäjä kirjoittaa tekstin "batman" ihan missä tahansa kohdassa dokumenttia, koko dokumentin sisältö vaihdetaan sellaiseksi, että siinä näytetään osoitteessa http://www.cs.helsinki.fi/u/avihavai/weso/batman.jpg oleva kuva. Muokkaa myös sivun tyylitiedostoa siten, että kuva täyttää koko ruudun. Alla muutama esimerkki siitä, miltä sivun tulee näyttää kun käyttäjä kirjoittaa merkkijonon "batman". Huomaa, että myös esimerkiksi kirjoitetun tekstin "Ilmoittautuisin mielelläni kurssille, nimeni on batman" tulee vaihtaa sivun sisältö näyttämään kuva.

Varmista esimerkiksi Chromen developer toolseissa olevaa Network-välilehteä käyttämällä, että noudat kuvan vasta kun käyttäjä on kirjoittanut syötteen "batman". Kuvaa ei saa hakea ennalta.

XSS

Jos sivustolla olevaan syötelaatikkoon (esimerkiksi keskustelupalstalla olevaan tekstikenttään) syötettyä dataa ei tarkisteta, ja se näytetään sellaisenaan myös muille käyttäjille, on sivuilla paha tietoturvaongelma. Cross-site-scripting -aukko, on tyypillinen web-ohjelmistoissa oleva tietoturva-aukko, joka mahdollistaa kolmannen osapuolen lähdekoodin lisäämisen sivuille.

Yllä näytetyllä tavalla on mahdollista toteuttaa esimerkiksi kalastelupalvelu, joka lähettää erilliselle palvelimelle kaiken käyttäjän syöttämän datan. Jos Batman-naaman sijaan näytettäisiin sivuilla normaalisti näytettävä "syötä käyttäjätunnus ja salasana"-lomake, ei käyttäjä osaa varautua siihen, että lomake ei ole "oikea" lomake -- ja salasanat ja tunnukset päätyvät vääriin käsiin.

Canvas ja piirtäminen

Canvas on yksi uusista HTML5-spesifikaation mukana tulleista elementeistä, ja se tarjoaa tuen grafiikan piirtämiselle JavaScriptin kautta. Tutustutaan canvas-elementin käyttöön.

Canvas-elementtiä määriteltäessä sille asetetaan tunnus, jotta siihen päästään käsiksi Javascriptillä, sekä leveys (width) ja korkeus (height). Alla olevalla määrittelyllä luodaan canvas-elementti, jonka tunnus on alusta, leveys 300 pikseliä, ja korkeus 200 pikseliä: <canvas id="alusta" width="300" height="200"></canvas>. Osana HTML-sivua se näyttää seuraavalta:

Canvas, johon ei ole piirretty vielä mitään, ei näytä juurikaan miltään. Se vain vie tilaa. Yllä olevalle canvas-elementille on määritelty lisäksi tyyli, joka lisää sille reunat.

canvas {
    border: 1px rgb(0, 0, 0) solid;
    padding: 1em;
}

Canvas-elementtiä voi muokata javascript-koodilla. Yllä olevan elementin tunnus on "alusta", joten siihen pääsee käsiksi komennolla document.getElementById("alusta"); tai $("#alusta")[0]. Canvas-elementtiin piirretään siihen liittyvän kontekstin kautta. Kontekstin saa canvas-elementistä getContext("2d")-komennolla. Kun olemme saaneet käyttöömme kontekstin, voimme asettaa sille erilaisia tyylejä, kuten piirtovärin, sekä käyttää valmiita piirtofunktioita. Piirtoväri ja tyylit asetetaan CSS:n avulla.

Piirtovärin voi asettaa kontekstille CSS-tyyleistä tutulla värikomennolla (konteksti.fillStyle="rgb(0, 0, 0)"), ja esimerkiksi suorakulmion voi piirtää komennolla (konteksti.fillRect(x-koordinaatti, y-koordinaatti, leveys, korkeus). Alla olevalla esimerkillä piirretään aiemmin määrittelemäämme ikkunaan neliö, jonka leveys ja korkeus on 50 pikseliä, sekä vasen yläkulma on koordinaatissa (100, 50).

Kokeile neliön x- ja y-koordinaattien vaihtamista. Piirrä tämän jälkeen neliö uudestaan. Mitä outoa huomaat?

Koordinaatisto

Toisin kuin perinteisessä koordinaatistossa, jossa origo visualisoidaan yleensä kuvan keskelle, canvas-elementissä koordinaatiston keskipiste on canvas-elementin vasemmassa ylälaidassa. Toinen poikkeus on se, että y-koordinaatin arvo kasvaa alaspäin mentäessä. Tämä johtuu siitä, että ikkunoiden kokoa muutetaan yleensä oikeasta alakulmasta. Jos y-koordinaatin arvo kasvaisi ylöspäin mennessä, siirtäisi ikkunan pienennys koko kuvaa. Oikeastaan canvas toimii samoin kuin lähes kaikkien ohjelmointikielten grafiikkakirjastot.

Alla on canvas-elementti, jonka tunnus on "koordinaatisto". Käytetään sitä koordinaatistossa sijainnin havainnointiin.

Uudelleen piirtäminen ja taustan tyhjennys

Asioita uudestaan piirrettäessä huomaamme, että vanhat asiat jäävät näkyviin. Tämä ei ole aina toivottua. Canvas-elementin konteksti tarjoaa metodin clearRect(x, y, leveys, korkeus) tietyn alueen tyhjentämiseen. Esimerkiksi koko ruudun tyhjentäminen onnistuu komennolla clearRect(0, 0, 300, 200), olettaen että ruudun leveys on 300 pikseliä, ja korkeus 200 pikseliä.

Alla oleva esimerkki tyhjentää yllä olevan koordinaatisto-elementin.

Osa selaimista tyhjentää canvas-elementin sisällön myös asettamalla canvas-elementin leveyden uudestaan canvas.width = canvas.width.

Laatikko

Luodaan ensimmäinen laatikko, ja muistellaan samalla JSON-kutsujen tekemistä. Älä enää tässä vaiheessa käytä synkronoituja ajax-kutsuja!

Luo sovellus, joka käynnistyessään hakee osoitteessa "http://www.cs.helsinki.fi/u/avihavai/weso/laatikko.json" määritellyn datan. Datan muoto on seuraava: ensimmäisessä sarakkeessa on x-koordinaatti, toisessa y-koordinaatti, kolmannessa leveys, ja neljännessä korkeus. Kun sovellus saa JSON-datan, se piirtää sen pohjalta laatikon tehtäväpohjassa olevaan canvas-elementtiin. Käytä värinä punaista.

Huom! Jos et käytä chromen disable-web-security -parametria, voit hakea datan myös osoitteesta "http://pure-escarpment-3768.herokuapp.com/app/laatikko".

Kun sovelluksesi näyttää samalta kuin yllä, palaute se TMC:lle. Vinkki: Vaikka tässä tehtävässä sitä ei vaadita, voi olla hyvä muistella miten olioita luodaan, ja esimerkiksi luoda oma Laatikko-konstruktorifunktio, jonka prototyypille määritellään metodi piirtämiselle. Piirtometodi voi saada parametrina esimerkiksi piirtokontekstin.

Erilaisia piirtofunktioita

Neliöiden lisäksi voimme piirtää mm. viivoja, ympyröitä ja kuvia.

Viivan piirtäminen

Alla olevan canvas-elementin tunnus on "viiva". Viivojen piirtäminen tapahtuu siirtämällä konteksti ensin aloituspisteeseen komennolla moveTo(x, y), tämän jälkeen viiva määritellään joukolla komentoja lineTo(seuraavaX, seuraavaY). Kun viiva on määritelty, kutsutaan funktiota stroke, joka lopulta piirtää viivan.

Ympyrän piirtäminen

Alla käytössämme on piirtoalusta tunnuksella "ovaali". Ympyröiden piirtäminen canvas-elementtiin tapahtuu funktion arc(alkux, alkuy, säde, alkukulma, loppukulma)-avulla. Funktio arc saa parametrina kaaren alkupisteen, säteen, kaaren aloituskulman, sekä kaaren lopetuskulman. Kulma määritellään radiaaneina, kulma 0 vastaa itää, Math.PI/2 etelää, Math.PI länttä, ja Math.PI * 1.5 pohjoista. Alla olevassa esimerkissä piirretään kokonainen ympyrä.

Huomaa, että komento stroke piirtää viivan. Voit täyttää ympyrän komennolla fill. Komennot beginPath ja closePath aloittavat ja lopettavat polun. Komentoa arc voi käyttää myös kiemurtelevien polkujen rakentamiseen. Emme kuitenkaan käsittele sitä tässä.

Kuvan piirtäminen

Kuvan piirtäminen onnistuu komennolla drawImage, jolle annetaan parametrina piirrettävä kuva, koordinaatti, johon kuvan piirtäminen aloitetaan, sekä haluttaessa kuvan koko. Esimerkiksi, jos kuvan leveys ja korkeus on 100, mutta piirtokomento on muotoa drawImage(kuva, 10, 10, 20, 20), kuvasta piirretään 20 pikselin levyinen ja korkuinen versio alkaen koordinaatista (10, 10).

Kuvan voi luoda ohjelmallisesti, tai sen voi hakea sivulla olevasta elementistä. Alla olevassa esimerkissä kuva luodaan ohjelmallisesti, ja sen lähteenä käytetään aiemmin näkemäämme megaman-kuvaa. Canvas-elementin tunnus on "kuvacanvas". Koska alla piirrettävälle kuvalle ei anneta kokoa, se piirretään alkuperäisen kokoisena.

Huom! Joillain selaimilla ylläoleva koodi ei toimi, sillä pelkän kuvaelementin luominen ei takaa sitä, että kuva oikeasti ladataan. Toinen lähestymistapa on lisätä kuvaelementti ensin sivulle, ja viitata siihen erillisen tunnuksen kautta esim. seuraavasti. Alla on käytetty JQueryä. Kun painat nappia, Megaman-kuva lisätään myös tämän dokumentin loppuun -- normaalisti kuvalle asetettaisiin lisäksi tyyli, joka piilottaa sen.

Kooste (2p)

Tässä tehtävässä jatketaan aiemmin tehtyä laatikon piirtämistä, mutta tällä kertaa laatikoista koostetaan erillinen kuvio.

Luo sovellus, joka käynnistyessään hakee osoitteessa "http://www.cs.helsinki.fi/u/avihavai/weso/kooste.json" määritellyn datan. Data sisältää listan suorakulmioita, joista jokaisen muoto on seuraava: ensimmäisessä sarakkeessa on x-koordinaatti, toisessa y-koordinaatti, kolmannessa leveys, neljännessä korkeus, ja viidennessä väri. Kun sovellus hakee JSON-datan, sen tulee piirtää datan pohjalta koostekuvio, joka sisältää kaikki kuviot.

Huom! Jos et käytä chromen disable-web-security -parametria, voit myös yrittää hakea dataa osoitteesta "http://pure-escarpment-3768.herokuapp.com/app/kooste".

Kannattaa muistella tässä tavara-matkalaukku-.. -tehtävää.

Kun saat piirrettyä kuvan, lisää pohjaan toiminnallisuus, jossa kuvaa voi liikuttaa nuolinäppäimillä. Tehtävän mukana tulee tiedosto keys.js, jossa on yksinkertainen näppäimistönhallintatoiminnallisuus. Voit toteuttaa myös oman toiminnallisuuden.

Kun painat näppäintä vasemmalle, kuvan tulisi siirtyä vasemmalle. Kun painat oikealle, kuvan tulisi siirtyä oikealle. Kun painat ylös, kuvan tulisi siirtyä ylös, ja alas painaessa kuvan tulisi siirtyä alas. Jos käytät tehtäväpohjassa olevaa näppäimistönkäsittelijää, alla olevasta animointia helpottavasta koodinpätkästä lienee sinulle hyötyä.

    /* ... */ 
    /* tapahtumakäsittelijöiden rekisteröinti siten, että tapahtumat kerrotaan keyhandlerille */
    /* ... */
    setInterval(function() {
        /* tätä funktiota kutsutaan noin 60 kertaa sekunnissa */
        /* näppäinten tarkistus keyhandlerilta ja tarvittu toiminta */
    }, 1000 / 60);
    /* ... */

Oleellista on toteuttaa sovellus siten, että nuolinäppäimillä tapahtuva siirtäminen siirtää aina koko kuviota. Muista myös hahmoa uudelleen piirrettäessä poistaa vanha piirros. Kun sovelluksesi toimii kuten haluttu, palauta se TMC:lle.

Fonttien piirtäminen

Canvas-elementtiin voi piirtää myös tekstiä. Tekstin piirtäminen tapahtuu kontekstin tarjoamalla metodilla fillText(teksti, alkux, alkuy), jolle annetaan parametrina piirrettävä teksti, sekä tekstin vasemointi. Kontekstille voi asettaa tekstin käyttämän fontin kontekstiin liittyvään font-attribuuttiin. Fontin määrittely on samankaltainen kuin CSS-kielessä, esimerkiksi 12 pikselin kokoisen Arial-tekstin voi asettaa komennolla konteksti.font = "12px Arial";.

Yllä olevan canvas-elementin tunnus on "tekstialue". Seuraava esimerkki piirtää siihen 20 pikselin Comic Sans-fontilla vihreän viestin "jea!". Fontti on käytössä vain jos se löytyy käyttäjän käyttöjärjestelmästä.

Graafien piirtäminen

Canvas-elementtiä käytetään yhä enemmän erilaisten graafien ja visuaalisointien piirtämiseen, mutta sen heikkous on se, että kun canvas-elementille on piirretty kuvio, kuvio unohdetaan, eikä siinä olevia elementtejä voi muokata suoraan. Visuaalisaatioiden toteuttaminen on kuitenkin helpohkoa. Esimerkiksi alla on toteutettu visualisaatio, joka hakee tähän kurssiin liittyvien opiskelijoiden tekemät tehtävät. Tehtyjen tehtävien lukumäärä löytyy TMC:n sivuilta osoitteesta http://tmc.mooc.fi/hy/courses/12/points.csv. TMC:n palvelin ei kuitenkaan tue Cross-origin pyyntöjä, joten datasta löytyy myös hieman vanha kopio osoitteessa "http://pure-escarpment-3768.herokuapp.com/app/points".

Alla olevan ohjelman monimutkaisin osa liittynee CSV-formaatin lukemiseen ja muokkaamiseen. Funktion plot tekemä lopullinen visualisointi on ns. hatusta vedetty.

function plot(data) {
    var konteksti = $("#piirtoalue")[0].getContext("2d");
    konteksti.strokeStyle="rgb(180, 0, 0)";
    for(var exerciseCount in data) {
        var x = exerciseCount * 8;
        var y = data[exerciseCount] * 8;
        var sade = data[exerciseCount];
        
        konteksti.beginPath();
        konteksti.arc(x, y, sade, 0, 2*Math.PI);
        konteksti.stroke();
        konteksti.closePath();
    }                
}

function parse(data) {
    var counts = {};
    var lines = data.split('\n');
    $.each(lines, function(index, line) {
        if(index === 0) {
            // skip header
            return;
        }
        
        line = line.replace(/"/g,'');
        var elements = line.split(',');                    

        var last = $(elements).last()[0];
        if(!counts[last]) {
            counts[last] = 1;
        } else {
            counts[last]++;
        }                    
    });  
    
    return counts;
}

$(document).ready(function() {
    $.ajax({
        url: "http://pure-escarpment-3768.herokuapp.com/app/points",
        success: function(data) {
            var counts = parse(data);
            console.log(JSON.stringify(counts));
            plot(counts);
        }
    });
});    

Voit tutustua ylläolevan skriptin luomaan graafiin painamalla alla olevaa nappia.

Graafien tekemiseen on toteutettu huomattava määrä valmiita JavaScript-kirjastoja. Tutustutaan seuraavaksi pikaisesti yhteen niistä.

Raphaël

Raphaël on työväline graafien piirtämiseen. Se hallinnoi itse piirtoaluetta, sekä tarjoaa piirtämiseen erilaisia animointi- ja piirtoapuvälineitä. Piirrettäviin kuvioihin voi lisäksi lisätä esimerkiksi erilaisia tapahtumankäsittelijöitä, mikä mahdollistaa interaktiivisten graafien luomisen.

Lähdetään perusteista liikkeelle, ja toteutetaan sovellus, joka piirtää neliön.

var canvas = Raphael(50, 50, 300, 300);

var laatikko = canvas.rect(50, 50, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");

Raphaël luo itse piirtoalueen joko olemassaolevan elementin sisälle, tai määrittelemäämme sijaintiin. Yllä määrittelemme sijainniksi 50 pikseliä vasemmasta reunasta ja yläreunasta, ja asetamme piirtoalustan kooksi 300 x 300 pikseliä. Tämän jälkeen luomme laatikon, jonka vasen yläkulma on 50 pikseliä piirtoalueen vasemmasta ja yläreunasta, ja jonka leveys ja korkeus on 40 pikseliä. Tämän jälkeen asetamme laatikon väriksi punaisen. Mielenkiintoista on, että piirtäminen tapahtuu nykyisen alueen päälle.

Elementteihin voi lisätä tapahtumankäsittelijöitä. Esimerkiksi metodilla click määritellään elementille funktio, jota kutsutaan kun elementtiä klikataan. Muokataan ylläolevaa esimerkkiä siten, että laatikkoa klikattaessa sen väri muuttuu vihreäksi.

var canvas = Raphael(50, 50, 300, 300);

var laatikko = canvas.rect(50, 50, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");
laatikko.click(function() {
    this.attr("fill", "rgb(0, 255, 0)");
});

Huomaa, että viittaamme funktion sisällä aina juuri tähän elementtiin, this-avainsanan avulla. Useampien kuvioiden lisääminen on mahdollista, alla luomme kaksi erillistä laatikkoa, jotka kummatkin vaihtavat värinsä vihreäksi kun niitä klikataan.

var canvas = Raphael(50, 50, 300, 300);

var laatikko = canvas.rect(50, 50, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");
laatikko.click(function() {
    this.attr("fill", "rgb(0, 255, 0)");
});

laatikko = canvas.rect(150, 150, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");
laatikko.click(function() {
    this.attr("fill", "rgb(0, 255, 0)");
});

Piirretään vielä viiva näiden kahden laatikon välille. Raphaëlin viivasyntaksi on hieman erityinen. Viivan piirtäminen tapahtuu merkkijonon avulla. Merkkijonoon asetetaan ensin "M"-etuliite, jota seuraa kaksi numeroa. Nämä yhdessä kertovat pisteen, mistä piirtäminen aloitetaan. Käytännössä merkintä on lyhenne lauseesta "MOVE X Y". Tätä seuraa yksi tai useampi komento, jossa on "L"-etuliite, ja kaksi numeroa. Tämä tarkoittaa "LINE X Y", eli piirretään edellisestä kohdasta viiva kohtaan (x, y).

Koordinaattien (0,0) ja (250,250) välille piirrettävä viiva onnistuu merkkijonolla "M 0 0 L 250 250". Itse viivan piirtäminen tapahtuu komennolla path, jolle annetaan parametrina merkkijono. Komento path palauttaa polkuolion, jolle voi asettaa erilaisia attribuutteja. Alla luodaan aiemmin kehitetystä merkkijonosta polku, jonka paksuus on 4 pikseliä.

var canvas = Raphael(50, 50, 300, 300);
var path = canvas.path("M 0 0 L 250 250");
path.attr("stroke-width", "4px");

Yhdistetään kaksi edellistä. Piirretään neliö siten, että neliöstä kulkee polku toiseen neliöön. Toteutetaan polun piirtäminen siten, että se piirretään ennen laatikoita, jolloin laatikot menevät lopulta mukavasti polun päälle. Lähtöpiste ja loppupiste on laskettu siten, että ne ovat aina laatikon keskellä.

var path = canvas.path("M 70 70 L 170 170");
path.attr("stroke-width", "4px");

var laatikko = canvas.rect(50, 50, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");
laatikko.click(function() {
    this.attr("fill", "rgb(0, 255, 0)");
});

laatikko = canvas.rect(150, 150, 40, 40);
laatikko.attr("fill", "rgb(255, 0, 0)");
laatikko.click(function() {
    this.attr("fill", "rgb(0, 255, 0)");
});

TreeHee! (2p)

Tehtäväpohjassa tulee mukana seuraavannäköinen data.

    var elements = [
        [300, 20, 15, false],
        
        [120, 140, 15, false],
        [280, 140, 15, false],
        [420, 140, 15, false],
        
        [60, 300, 15, false],
        [130, 300, 15, true],
        [210, 300, 15, true],
        [270, 300, 15, true],
        [330, 300, 15, false],
        [380, 300, 15, false],
        [480, 300, 15, false]
    ];
    
    var childToParents = {
        0: [],
        1: [0],
        2: [0],
        3: [0],
        4: [1],
        5: [1],
        6: [1, 2],
        7: [2],
        8: [2],
        9: [3],
        10: [3]
    };

Ensimmäinen olio, eli lista elements, sisältää ympyröiden sijainteja. Jokaisella ympyrällä on keskipiste (kaksi ensimmäistä arvoa), säde (kolmas arvo), sekä tieto siitä, tuleeko värin olla harmaa vain vihreä. Jos neljäs arvo on false, tulee värin olla harmaa, jos true, niin vihreä. Toinen olio, eli olio childToParents sisältää assosiatiivisen taulukon, jossa jokainen avain vastaa ympyrää listassa elements. Jokaisen avaimen arvo on lista solmuista, joihin ympyrästä tulee piirtää viiva -- eli lista vanhemmista.

Tehtävänäsi tässä tehtävässä on piirtää seuraavannäköinen puu yllä määritellyn datan pohjalta. Tutustu ensin rauhassa ympyrän piirtoon liittyvään dokumentaatioon, ja lähde liikkeeseen siitä, että piirrät listan elements määrittelemät ympyrät. Kun olet saanut ympyrät piirrettyä, piirrä tämän jälkeen viivat ympyröiden välille olion childToParents avulla.

Kun olet valmis, palauta tehtävä TMC:lle. Värien ei tarvitse olla täsmälleen samat kuin ylläolevassa kuvassa.

Pelit!

Pelien ei aina tarvitse olla räimettä ja räiskintää, vaan ne voivat liittyä myös hyödyllisiin asioihin. Seuraava tehtävä kertaa muutamaa aiemmin tutuksi tullutta asiaa.

Typist (2p)

Käy katsomassa osoitteessa http://www.youtube.com/watch?v=m9EXEpjSDEw oleva video. Tässä tehtävässä tehtävänäsi on viimeistellä kymmensormijärjestelmän harjoitteluohjelman toteutus. Järjestelmään on jo toteutettu runko, joka sisältää mm. tällä hetkellä kirjoitettavien sanojen korostamisen, sekä ainakin osan tarvittavista muuttujista.

Muokkaa ja täydennä tehtävää siten, että kun käyttäjä painaa välilyöntiä "inputter"-tunnuksella merkityssä tekstikentässä, tekstikentässä oleva arvo tarkistetaan vertaamalla sitä sanaan, joka tällä hetkellä pitäisi kirjoittaa. Jos sanan kirjoitus onnistuu, kasvata oikein menneiden sanojen määrää yhdellä. Kun käyttäjä on painanut välilyöntiä, ja kirjoitettu sana on tarkistettu, siirrytään seuraavaan sanaan. Tutustu jo toteutettuun ohjelmakoodiin, siitä on todennäköisesti jotain hyötyä.

Sovelluksen tulee myös näyttää statistiikkaa kirjoitetuista sanoista. Tunnuksella "wpm" (Words Per Minute) merkittyyn kenttään tulee asettaa oikein menneiden sanojen määrä jaettuna tähän asti kuluneella ajalla sekunneissa kerrottuna kuudellakymmenellä. Tunnuksella "correct" merkittyyn kenttään tulee asettaa oikein menneiden sanojen määrä, ja tunnuksella "seconds" merkittyyn kenttään tähän mennessä kuluneet sekunnit. Voit päivittää statistiikan kerran sekunnissa tehtäväpohjan lopussa olevan setInterval-komennon avulla.

Pelin tulee käynnistyä kun sivu ladataan. Kun sivu ladataan, tulee fokuksen olla heti "inputter"-tekstikentässä. Tähän auttaa esimerkiksi JQueryn funktio focus. Kun sovelluksesi toimii kuten toivottu, ja näyttää esimerkiksi seuraavalta, palauta se TMC:lle.

Pelimoottorin rakenne

Käytännössä lähes jokaisen interaktiivisen pelin rakenne on jaettu kolmeen erilliseen osaan: (1) käyttäjän syötteiden tarkastamiseen, (2) pelitilanteen päivittämiseen, ja (3) pelin piirtämiseen. Pelimoottorin tehtävänä on toistaa näitä kolmea osaa yhä uudestaan ja uudestaan, mikä yhdessä peliin ohjelmoidun toiminnallisuuden kanssa luo illuusion jatkuvasta toiminnasta. Kaikki pelien tapahtumat ja toiminnallisuudet ovat ohjelmoijien toteuttamia, eli mikään ei synny itsestään.

Pohditaan jokaista osaa hieman erikseen.

Käyttäjän syötteiden tarkastaminen

Käyttäjän syötteisiin reagoinnin toteuttaminen riippuu luonnollisesti toteutettevasta pelistä. Koska käyttäjän syötteisiin (esimerkiksi napin painallus) liittyvät tapahtumat ovat toteutettu osittain käyttöjärjestelmän tai selaimen puolesta, tulee pelin puolella toteuttaa näille käsittely. Riippuen pelin tarpeista, yksi lähestymistapa on toteuttaa erillinen tapahtumajono, johon asetetaan tapahtumia sitä mukaa kun niitä saapuu käyttöjärjestelmältä. Pelin logiikkavaiheessa käsitellään näitä jollain toivotulla tahdilla.

Toinen lähestymistapa on pitää yllä tilaa mahdollisista syötteistä. Esimerkiksi tehtävässä "Kooste" mukana ollut keys.js pitää yllä tilaa painetuista napeista. Ajatuksena tilan ylläpidossa on asettaa nappia painettaessa nappiin liittyvä yksikäsitteinen tunniste arvoksi true, jolloin napin ollessa pohjassa sovellus voi katsoa "onko nappi pohjassa". Kun käyttäjä päästää napista irti, eli nappi nousee, asetetaan nappiin liittyvä tunniste arvoksi false.

var keyhandler = (function() {
    var keys = new Array();
    var i = 0;
    while(i < 256) {
        keys[i] = false;
        i = i + 1;
    }

    function up() {
        return keys[38] || keys[175] || keys[87];
    }

    function down() {
        return keys[40] || keys[176] || keys[83];
    }

    function left() {
        return keys[37] || keys[178] || keys[65];
    }

    function right() {
        return keys[39] || keys[177] || keys[68];
    }
    
    function keydown(keycode) {
        keys[keycode] = true;
    }
    
    function keyup(keycode) {
        keys[keycode] = false;
    }

    function getMovement() {
        var movement = [0, 0];
        if(up()) {
            movement[1] = -1;
        }
        if(down()) {
            movement[1] = 1;
        }
        if(left()) {
            movement[0] = -1;
        }
        if(right()) {
            movement[0] = 1;
        }
        
        return movement;
    }

    return {
	up: up,
        down: down,
        left: left,
        right: right,
        
        keydown: keydown,
        keyup: keyup,
        getMovement: getMovement
    };
})()

Osa tapahtumista on sellaisia, että niillä ei ole konkreettista tilaa. Esimerkiksi hiiren liikkuminen on tapahtuma, joka on vain hetkellinen, ja se kannattaa asettaa osaksi käsiteltävät tapahtumat sisältävää jonoa. Hiiren liikkumista voi tarkistella osana elementtiä, tai osana koko dokumenttia. Alla olevassa esimerkissä dokumentille rekisteröidään hiiren liikettä seuraava tapahtumankäsittelijä. Tapahtumankäsittelijä tallentaa liikkeet erilliseen listaan, jonka nimi tässä on "engine.events".

  $(document).mousemove(function(eventInformation) {
      engine.events.push(["mousemove", eventInformation.pageX, eventInformation.pageY]);
  }

Pelitilanteen päivittäminen

Pelitilanteen päivittämisvaiheessa liikutetaan omaa hahmoa, mahdollisia muita hahmoja, sekä suoritetaan hahmojen interaktioon liittyvää logiikkaa. Oletetaan että hahmomme on yksinkertainen piste, ja että siitä on luotu ilmentymä seuraavanlaisella konstruktorilla.

function Hahmo(x, y) {
    this.x = x;
    thix.y = y;
}

Hahmo.prototype.siirra = function(siirto) {
    this.x += siirto[0];
    this.y += siirto[1];
}

Itse siirtotoiminnallisuus voi olla hyvinkin yksinkertainen. Alla käytämme yllä olevasta Hahmo-konstruktorista luotua ilmentymää sekä näppäimistönkäsittelijän tarjoamaa getMovement-funktiota.

    /* siirtäminen */
    hahmo.siirra(keyhandler.getMovement());

Jos pelissä on useampia hahmoja, niitä säilytetään listassa ja niitä siirretään luonnollisesti yksi kerrallaan. Jos osa hahmoista on tietokoneen hallinnoimia, ja niillä on oma siirtymislogiikka, voidaan niitä hallinnoida osana erilaista listaa. Törmäysten tarkastaminen onnistuu samalla tavalla. Jos pelissä on useampia hahmoja, kannattaa luoda hahmolle erillinen metodi, joka tarkistaa törmätäänkö toiseen hahmoon. Yhden pikselin hahmoilla tämä on helppoa.

Hahmo.prototype.tormaako = function(hahmo) {
    return this.x === hahmo.x && this.y === hahmo.y;
}

Jos hahmon koko on suurempi kuin 1 pikseli, perinteinen törmäyksen tarkistus rakennetaan siten, että luodaan pienin mahdollinen laatikko, joka sisältää hahmon. Tämän jälkeen tarkistetaan, onko toisen hahmon laatikko tähän hahmoon liittyvän laatikon ulkopuolella jokaista sivua yksitellen tarkastellen.

Piirtäminen

Piirrettäessä kannattaa muistaa ensin tyhjentää vanha grafiikka (tai piirtää poistettavan päälle), jonka jälkeen piirretään pelin grafiikkaa siten, että taaimmaisena olevat asiat (esim tausta) piirretään ensin. Tällöin oleelliset ja lähellä olevat hahmot eivät jää muiden hahmojen alle. Pelihahmoille kannattaa lisätä oma piirtometodi, joka saa parametrina piirtokontekstin.

Hahmo.prototype.piirra = function(konteksti) {
    konteksti.fillStyle = /* aseta piirtotyyli */
    konteksti.fillRect(this.x, this.y, 3, 3);
}

Ajastaminen

Ylläolevat askeleet tapahtuvat monta kertaa sekunnissa. HTML5 on tuomassa mukanaan paremman animointituen: funktio requestAnimationFrame tarjoaa ajastuksen, joka pyrkii noin 60 tapahtumaan sekunnissa. Jotta maailma ei olisi liian helppo, ei requestAnimationFrame toimi vielä kunnolla nykyaikaisissa selaimissa -- jokaisella selaintoteuttajalla on hieman omanlainen versionsa siitä. Kuten tyylitiedostoissakin, voimme onneksi toteuttaa version, joka pyrkii käyttämään saatavilla olevaa requestAnimationFrame-funktiota. Jos yhtäkään ei löydy, käytetään setTimeout-funktiota.

window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       || 
           window.webkitRequestAnimationFrame || 
           window.mozRequestAnimationFrame    || 
           window.oRequestAnimationFrame      || 
           window.msRequestAnimationFrame     || 
           function(/* kutsuttava funktio */ callback, /* elementti */ element){
               window.setTimeout(callback, 1000 / 60);
           };
    })();

Yllä luodaan erillinen requestAnimFrame-funktio, joka asetetaan osaksi koko dokumenttia. Funktiota requestAnimFrame kutsuttaessa selain kutsuu parametrina annettavaa funktiota uudestaan siten, että funktiokutsuja tulee (kevyehköissä peleissä) noin 60 sekunnissa. Pelimoottorin runko voisi näyttää esimerkiksi seuraavalta:

engine.tick = function() {
    engine.handleInputs();
    engine.handleLogic();
    engine.draw();
    requestAnimFrame( engine.tick );
}

Yllä kuvattu lähestymistapa toimii suuressa osassa pelejä. Alla on yksinkertainen runko kokonaisuudessaan. Rungossa ei ole tapahtumankäsittelyä, eikä logiikkaa. Piirtäminenkin on lähinnä vain esimerkki.

window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       || 
        window.webkitRequestAnimationFrame || 
        window.mozRequestAnimationFrame    || 
        window.oRequestAnimationFrame      || 
        window.msRequestAnimationFrame     || 
        function(/* kutsuttava funktio */ callback, /* elementti */ element){
        window.setTimeout(callback, 1000 / 60);
    };
})();  

var lander = {
    character: {
        x: 0,
        y: 0
    },
    events: [],
    input: function() {
        console.log("could handle e.g. an event from events-list");
    },
    logic: function() {
        console.log("brraaiinnss...");
    },
    render: function() {
        // get context and clear
        var context = $("#lunar")[0].getContext("2d");
        context.clearRect(0, 0, 400, 400);
        
        // do awesome painting -- would be smarter to implement
        // a separate character instance
        context.fillStyle = "rgb(0, 0, 0)";
        context.fillRect(lander.character.x, lander.character.y, 10, 10);                    
    },
    tick: function() {
        lander.input();
        lander.logic();
        lander.render();
        requestAnimFrame(lander.tick);
    }                
};

$(document).ready(function() {
    lander.tick();
});

Kuten huomaat, sovellus itsessään ei ole pakosti kovin iso. Kannattaa kuitenkin pohtia, voiko sen pilkkoa pienempiin osiin esimerkiksi siten, että syötteidenkäsittely toteutetaan omassa tiedostossaan, logiikka omassa tiedostossaan, ja renderöinti omassa tiedostossaan.

Highway Crossing Frog (4p)

Tässä tehtävässä tehtävänäsi on toteuttaa Highway Crossing Frog-peli, eli Frogger. Jos peli ei ole ennalta tuttu, käy katsomassa osoitteessa http://www.youtube.com/watch?v=okm0VtF2gH8 oleva video. Toteutettavan Frogger-pelin toiminnallisuus ja grafiikat ovat hieman yksinkertaisemmat.

Pelissä tulee vasemmalta laidalta punaisia autoja (laatikoita), jotka liikkuvat sulavasti oikealle ohjelmoijan päättämällä vauhdilla. Pelaaja ohjaa vihreää sammakkoa (laatikkoa). Kun pelaaja painaa nappia ylöspäin, sammakon tulee liikkua askel ylöspäin, vasemmalle painettaessa vasemmalle jne. Toteuta sammakon liikkuminen siten, että sammakko liikkuu aina yhden sammakon (ja laatikon) kokoisen ruudun kerrallaan. Autojen tulee liikkua jatkuvasti, ja autoja lisätään kentälle satunnaisella -- ohjelmoijan päättämällä -- tahdilla.

Sammakon liikkeessä ei tarvitse olla animointia, vaan se liikkuu aina kokonaisen sammakonmitan kerrallaan.

Jos sammakko jää auton alle, eli sammakko osuu autoon, peli loppuu ja käyttäjälle näytetään viesti "Spläts!". Jos sammakko pääsee ylös eli toiselle ruskealle alueelle, käyttäjälle tulee näyttää viesti "Voitit!".

Kun pelisi on pelattavassa kunnossa, eli autot kulkevat, sen voi voittaa, ja siinä voi hävitä, ja se näyttää kutakuinkin ylläolevalta peliltä, palauta se TMC:lle.

Oma media

Idean ja interaktion lisäksi peleissä isoa roolia pelaa pelin sisältämä media, eli kuvat, animaatiot ja äänet. Netissä on muutamia sivustoja, joiden tarkoituksena on kerätä materiaalia vapaaseen käyttöön. Esimerkiksi OpenGameArt.Org tarjoaa erilaisia tekstuureja ja äänitiedostoja ohjelmistokehittäjien käyttöön. Materiaalia netistä etsittäessä kannattaa pitää mielessä materiaalien lisenssit, ja käyttää vain sivustoja ja materiaaleja, jotka tarjoavat tiedot materiaalien lisensseistä.

Fontit

Normaalien käyttöjärjestelmissä olevien fonttien lisäksi web-suunnittelijat voivat käyttää nykyaikaisissa selaimissa CSS3:n mukana tullutta fonttitukea. CSS3 mahdollistaa fonttityylin määrittelyn @font-face-valitsimen avulla. Esimerkiksi, jos haluamme käyttää osoitteessa "data/bigelow_rules.woff" olevaa Bigelow Rules-fonttia (lähde: Astigmatic), tulee meidän määritellä sille ensin oma tyyli.

@font-face {
    font-family: BigelowRules; 
    src: url('data/bigelow_rules.woff'); 
}

Kun tyylimäärittely on tehty, voimme käyttää uutta fonttia osana omaa sivuamme. Esimerkiksi alla määritellään h1-elementille tyyli, jossa käytössä on aiemmin määritelty BigelowRules-fontti. Alla oleva esimerkki toimii ainakin kun selaimessa on asetettu CORS-tuki päälle.

h1 {
    font-family: BigelowRules, sans-serif; /* sans-serif toimii varavaihtoehtona */
}

Ta-daa! Pangrams!

WOFF on web-fonteille tarkoitettu avoin tiedostomuoto. Esimerkiksi Google pitää yllä arkistoa vapaasti käytettävissä olevista web-fonteista osoitteessa http://www.google.com/webfonts.

KiteOne

Etsi "Kite One"-fontti Googlen Web Fonts-palvelusta, ja lisää se tehtäväpohjassa olevalle sivulle. Fontin tulee vaikuttaa vain otsakkeessa olevaan tekstiin "Kite One!". Kun fontti on käytössä, sivusi tulee näyttää kutakuinkin seuraavalta.

Kun olet saanut uuden fontin lisättyä, ja sivusi näyttää ylläolevalta, palauta tehtävä TMC:lle.

Animaatiot

Web-sivujen alkuaikoina animaatioihin käytettiin gif-kuvia, jotka sisälsivät animaation muodostavan kuvasarjan. Peleissä käytettävissä animaatioissa käytetään itseasiassa jopa hieman vanhempaa tekniikkaa. Sprite-kuvat sisältävät yhden tai useamman kuvan ("sprite-sheet"), jotka kuvaavat hahmoa erilaisissa asennoissa. Näitä kuvia vaihtamalla luodaan illuusio liikkeestä.

Kuvien vaihtaminen onnistuu käyttämällä yhtä isompaa kuvaa, josta valitaan canvas-elementin drawImage-funktiolla aina sopiva osa. Syntaksi kuvan osan piirtämiseen on seuraava.

context.drawImage(kuva, x-koordinaatti-kuvasta, y-koordinaatti-kuvasta,
        kaytettavan-spriten-leveys, kaytettavan-spriten-korkeus,
	x-koordinaatti-canvaksella, y-koordinaatti-canvaksella,
	spriten-leveys-canvaksella, spriten-korkeus-canvaksella);

Alla on Andrey Pomazanin luoma spritesarja juoksevasta joulupukista. Koko sarja sisältää 16 kuvaa, joita vuorottelemalla saadaan aikaan animaatio. Yksittäisen kuvasarjan käyttö useamman kuvan sijaan helpottaa lataamista, sillä selaimen tarvitsee hakea vain yksi kuva.

Periaatteessa pelihahmon ja siihen liittyvän animaation luominen sisältää pelihahmoon liittyvän olion luomisen. Yksi vaihtoehto olisi kytkeä animaatio mukaan pelihahmon koodiin, mutta ylläpidettävyydeltään se ei olisi järkevää. Animaatio on oma kokonaisuutensa, joten sitä varten kannattanee luoda oma olio. Luodaan animaatiotoiminnallisuus, joka saa parametrina spriten sisältävän kuvan sekä yksittäisen spriten leveyden ja korkeuden. Animaatio tarjoaa toiminnot "next" joka vaihtaa seuraavaan kuvaan, sekä "draw", joka piirtää hahmon parametrina annettuun kontekstiin ja sijaintiin.

           
function Animation(image, frame_width, frame_height) {
    this.image = image;
    this.frame_width = frame_width;
    this.frame_height = frame_height;
                
    this.current_x = 0;
    this.current_y = 0;
}
            
Animation.prototype.next = function() {
    this.current_x += this.frame_width;
    if(this.current_x < this.image.width) {
        return;
    }
                
    this.current_x = 0;
    this.current_y += this.frame_height;
                
    if(this.current_y >= this.image.height) {
        this.current_y = 0;
    }
}
            
Animation.prototype.draw = function(context, xposition, yposition) {
    context.drawImage(this.image, this.current_x, this.current_y, 
                      this.frame_width, this.frame_height, 
                      xposition, yposition, 
                      this.frame_width, this.frame_height);
}

Uuden animaatio-olion luominen onnistuu helposti. Yksittäinen kuva joulupukkisarjassa on 150 pikseliä leveä, ja 150 pikseliä korkea. Kuva on img-elementissä, jolla on tunnus "santasprite". Hahmotelllaan animaation käyttöä.

    var animation = new Animation($("#santasprite")[0], 150, 150);
    var context = $("#canvas")[0].getContext("2d");

    setInterval(function() {
        // tyhjennä aiempi kuva
        animation.next();
        animation.draw(context, 0, 0);
    }, 1000/20); // animoidaan 20 kertaa sekunnissa

Canvas-elementissä animaatio näyttää seuraavalta:

Käytännössä pelihahmoja ei kannata ajatella puhtaasti animaatio-olioina, vaan niitä varten kannattaa luoda olio, joka kapseloi hahmoon liittyvän animointi-olion.

function Character(){
    this.x = 0;
    this.y = 200;
    this.animation = new Animation($("#...")[0], 150, 150);
}

Character.prototype.draw = function(context) {
    this.animation.draw(context, this.x, this.y);                
}


Character.prototype.move = function(movement) {
    this.x += movement[0];
    this.y += movement[1];
}

Character.prototype.animate = function() {
    this.animation.next();
}

Taustan animointi

Ajatuksena taustan animoinnissa on yleensä yhden taustakuvan käyttäminen siten, että se piirretään yhä uudestaan ja uudestaan. Tausta voi koostua yhdestä tai useammasta kuvasta, joita siirretään tiettyä vauhtia. Kauempana horisontissa olevat kuvat liikkuvat hitaammin, lähempänä olevat nopeammin. Taustakuvissa ei yleensä ole erityistä animaatiota: pelkkä taustan siirtyminen luo illuusion liikkeestä maailmasta. Lisätään ylläolevaan animaatioon pilvi, joka lipuu hitaasti pukin ohi.

Käytetään seuraavaa kuvaa pilvenä.

Animaatiokoodin ei tarvitse olla monimutkainen sillä kuva ei muutu. Yksinkertaisimmillaan kuvan voi ladata erilliseen muuttujaan, ja pitää yllä sen x-koordinaattia. Kuvan siirtyminen tapahtuu x-koordinaattia muuttamalla.


// ...
var cloud = $("#cloud")[0];
var cloud_x = 200;
// ...

setInterval(function() {
    // siirrä
    cloud_x -= 1;

    if(cloud_x < -200) {
        cloud_x = 200;
    }

    // piirrä
    context.clearRect(0, 0, 150, 150);
    // ...
}, 1000/20);

Kuitenkin, jos käytössäsi on useampia taustakuvia, kannattanee niitä varten luoda sopiva taustakuvien liikuttamista helpottava olio -- ylläoleva lähestymistapa johtaa ennenpitkää hyvin sotkuiseen koodiin.

Taivas

Tehtäväpohjan mukana tulevassa animaatiossa joulupukki juoksee yksinäisen näköisesti. Lisää animaatioon tausta, joka liikkuu oikealta vasemmalla. Käytä taustakuvana img-kansiossa olevaa "taivas.jpg" -kuvaa. Taustan tulee siirtyä vasemmalle noin 30 pikseliä sekunnissa.

Säilytä animaatiossa olevan pilven ja pukin toiminnallisuus ennallaan. Alla on kuvia pukista taivaan kanssa.

Kannattanee piirtää taustakuva kahdesti siten, että toinen kuva seuraa aina toista. Kun ensimmäinen taustakuva on valunut kokonaan pois näytöltä, se siirretään toisen kuvan oikealle puolelle. Kun olet valmis ja animaatio kulkee sujuvasti, palauta tehtävä TMC:lle.

Useampi animaatio

Hahmoilla on usein erilaisia toimintoja kuten käveleminen, hyppääminen ja kyykkyyn meneminen. Kunkin näistä tulee olla itsenäinen muista, mutta kuitenkin saatavilla, jotta animaatiosarjaa voi vaihtaa tarvittaessa. Pelistä riippuen hahmo voi liikkua myös eri suuntiin, jolloin näytettävän kuvasarjan tulee olla erilainen suunnasta riippuen. Alla oleva kuvasarja (tekijä Radomir Dopieralski) sisältää useita kävelyanimaatioita samassa tiedostossa. Jotta saamme animaation toimimaan, tulee animaation käsittelyä muokata siten, että kuhunkin animaatioon liittyy oma tila.

Yksi vaihtoehto animaatioiden luomiseen on luoda lista eri animaatioista.

var animationData = [
    {
        key: "WALK_DOWN",
        frames: [[0, 0], [0, 48], [0, 96], [0, 144]]
    },
    {
        key: "WALK_LEFT",
        frames: [[48, 0], [48, 48], [48, 96], [48, 144]]
    }, 
    // ...
]

Tällöin myös animaatiotoiminnallisuutta tulee muuttaa. Muokataan aiempaa animaatio-oliota siten, että se pitää tilaa valitusta animaatiosta ja tällä hetkellä käytössä olevasta kuvasta.

function Animation(image, data) {
    this.image = image;
    this.animations = {};
    this.frame = 0;
    this.animation_key = data[0].key;
    
    for(var i = 0; i < data.length; i++) {
        this.animations[data[i].key] = data[i].frames;
    }
}

Animation.prototype.next = function() {
    this.frame++;
    if(this.frame >= this.animations[this.animation_key].length) {
        this.frame = 0;
    }
}

Animation.prototype.draw = function(context, xposition, yposition) {
    var current_frame = this.animations[this.animation_key][this.frame];
    
    var x = current_frame[0];
    var y = current_frame[1];
    
    context.drawImage(this.image, x, y, 
            48, 48, // hardcoded numbers, code smell..
            xposition, yposition, 
            48, 48);
};

Nyt käytössä olevaa animaatiota voi muokata asettamalla animaatioon liittyvän animation_key muuttujan arvon erilaiseksi. Esimerkiksi jos näppäimistöltä painetaan nuolta vasemmalle, hahmon animaatioksi valitaan "WALK_LEFT" -animaatio, jota toistetaan kunnes jotain toista nappia painetaan.

Dude

Tutustu aluksi tehtäväpohjassa tulevaan koodiin -- se ei seuraa edellistä esimerkkiä, vaan rakentaa aiemman tehtävän pohjalle. Ohjelmakoodi lienee kuitenkin tuttua pienen tutustumisen jälkeen. Käytettävän kuvatiedoston tekijä on "Beshr Kayali".

Tässä tehtävässä sinun tulee lisätä tehtäväpohjan mukana tulevaan animaatioon toiminnallisuus, jossa välilyönti aiheuttaa pelihahmon hypyn. Pelihahmolle on hyppyä varten erillinen animaatiotiedosto "dude_jumping.jpg", joka löytyy kansiosta "img". Toteuta hyppyanimaatio siten, että lisäät hahmolle erillisen "JUMP"-animaatiotyypin, joka käynnistyy kun käyttäjä painaa välilyöntiä.

Kun olet saanut hyppyanimaation toimimaan välilyönnistä, lisää siihen toiminto, jossa hahmo nousee ylöspäin 10 pikseliä jokaista hyppyanimaation ensimmäistä viittä kuvaa kohti. Tämän jälkeen hahmon tulee laskeutua alaspäin 10 pikseliä seuraavilla viidellä kuvalla. Kun hyppyyn liittyvä animaatio on ohi, aiemmin käytössä olleen kävelyanimaation tulee jatkua.

Huom! Varmista, että hyppyanimaatio käynnistyy jos ja vain jos hahmo on kävelemässä, ja sen jalat ovat "alkuperäisellä" tasolla.

Alla on kuvia dudesta.

Kun olet valmis, palauta tehtävä TMC:lle.

Äänet

Äänet luovat ison osan peleihin ja ohjelmiin liittyvistä tunteista. HTML5 tarjoaa audio-elementin, jonka avulla sivuille voi lisätä ääntä. Yksinkertaisimmillaan audio-elementille annetaan parametrina lähdetiedosto, josta musiikkia soitetaan. Esimerkiksi seuraava elementti sivulla lataa kansiossa "data" olevan "fishing.wav"-tiedoston, ja alkaa soittamaan sitä heti kun lataus on onnistunut (autoplay), jatkaen alusta kun kappale on päässyt loppuun (loop). Audio-elementin attribuuteille ei tarvitse erikseen asettaa arvoja.

    <audio src="data/fishing.wav" autoplay loop></audio>

Tuetut audioformaatit riippuvat käytössä olevasta selaimesta ja käyttöjärjestelmästä. Esimerkiksi wav-tiedostot eivät toimi kaikissa firefox-selaimissa. Audio-elementti pyrkii välttämään audioformaattiongelmia tarjoamalla tuen useammalle lähdeelle. Audio-elementin sisälle voi määritellä eri lähteitä source-elementin avulla. Elementille lisätty määre "controls" näyttää mm. play-nappulan.

    <audio id="bat" controls>
        <source src="bat.wav" type="audio/wav">
        <source src="bat.mp3" type="audio/mp3">
    </audio>

Audio-elementtiä pystyy käyttämään ohjelmallisesti. Elementillä on mm. play-funktio, jolla kappaleen saa aloitettua. Esimerkiksi allaoleva lähdekoodi soittaa tunnuksella "bat" merkityn audio-elementin.

    $("#bat")[0].play();

Lumbergh

Luo alla olevan kuvan näköinen sivu. Kuhunkin kuvaan tulee liittyä klikkauksen kuuntelija. Vasemmanpuolimmaista kuvaa klikattaessa tulee soittaa "audio" -kansiosta äänitiedosto "mmyeah.mp3", keskimmäistä klikattaessa äänitiedosto "come-in.mp3", ja oikeanpuoleista kuvaa klikattaessa äänitiedosto "thanks.mp3".

Kun sovelluksesi toimii kuten köyhän miehen Bill Lumbergh -soundboard, palauta se TMC:lle.

Audio-elementtien lataaminen onnistuu myös ohjelmallisesti. Allaoleva lähdekoodi lataa audio-elementin, ja aloittaa sen soittamisen kun lataus on valmis.

var audio = $("<audio>").attr("src", "data/fishing.wav");
audio.load();

// audio-elementti luo tapahtuman "load" kun lataus on valmis
// -- kuunnellaan sitä
audio.addEventListener("load", function() { 
    audio.play();
}, true);

Audio-elementtiin liittyy muutamia ongelmia. Sen JavaScript-API ei sisällä stop-funktiota, ja useampia kappaleita päällekkäin soitettaessa musiikista tulee helposti särisevää. Tämän lisäksi ongelmana on yhteensopivuus taaksepäin: vanhemmat selaimet eivät tue audio-elementtiä. Näiden ongelmien ratkaisemiseksi on kirjoitettu apukirjastoja. Esimerkiksi Buzz osaa puskuroida audiokappaleet ja soittaa ääniä myös vanhemmilla selaimilla.

WebGL

WebGL on Khronos-ryhmän ylläpitämä avoin standardi 3d-grafiikan piirtämiseen web-sivuilla. Se perustuu OpenGL ES 2.0-spesifikaatioon, jota on käytetty mm. mobiilisovelluksissa. WebGL:ää käytetään canvas-elementin kautta, ja sen ymmärtäminen vaatii hieman ymmärrystä OpenGL:stä ja matriisilaskennasta -- tämä kappale ei ole tulossa kokeeseen, ja sen näyttää vain muutamia suuntia (WebGL:n kattavampi käsittely tarvitsisi hieman enemmän aikaa...). Jos OpenGL kiinnostaa, kannattanee tutustua NeHen sivustoon osoitteessa http://nehe.gamedev.net/.

Yksinkertaisimmillaan WebGL-sovelluksen saa käyttöön luomalla canvas-elementin, ja pyytämällä siltä kontekstia. WebGL-kontekstia pyydettäessä emme pyydä "2d"-kontekstia, vaan kontekstia "experimental-webgl". Liite "experimental" liittyy siihen, että selaintuki on vielä kokeellisella asteella. WebGL vaatii koneelta toimiakseen melko uuden näytönohjaimen, sopivat ajurit, ja WebGL:ää tukevan selaimen.

$(document).ready(function() {
    var gl = $("#canvas")[0].getContext("experimental-webgl");

    if(!gl) {
        alert("valitettavasti selaimessasi ei ole webgl-tukea :(");
        return;    
    }

    // taustan tyhjennys, väriksi punainen, ei läpinäkyvyyttä
  
    gl.clearColor(1, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
}

Jos selaimesi tukee WebGL:ää, muuttuu seuraava canvas-elementti punaiseksi kun klikkaat sitä. Muuten näet viestin, joka kertoo ettei tukea löydy.

Komento clearColor määrittelee canvas-elementin taustavärin, ja se saa neljä parametria. Kolme ensimmäistä arvoa ovat punaisen, vihreän, ja sinisen määrä lukuna nollan ja yhden välillä, ja neljäs on taustan läpinäkyvyys nollan ja yhden välillä. Arvo 1 on täysin läpinäkymätön tausta. Komento clear tyhjentää canvas-elementtiin liittyvän datan. Parametri gl.COLOR_BUFFER_BIT poistaa canvakseen liittyvät pikselit (värit).

Halutessamme luoda ohjelmaan konkreettista sisältöä, joudumme ohjelmoimaan valitettavasti huomattavasti enemmän. WebGL käyttää vahvasti shadereita, eli näytönohjaimella suoritettavia pieniä ohjelmia, piirrettävän kuvan käsittelyyn. Shaderit kirjoitetaan GLSL-kielellä, joka on niitä varten luotu kieli. Shadereiden tarjoamiin mahdollisuuksiin voi tutustua Shader Toyn sivuilla osoitteessa http://www.iquilezles.org/apps/shadertoy/.

Käytännössä 3d-kuvien luonti tapahtuu antamalla ohjelmalle JavaScriptin avulla joukko pisteitä. Kullekin pisteelle kutsutaan ensin vertex shaderia, jonka tehtävänä on mm. päättää, mihin kohtaan piste tulee näytöllä. Vertex shaderin suorituksen jälkeen pisteistä luodaan ruudulle sopiva 2d-versio, jonka jälkeen jokaiselle pikselille kutsutaan fragment shaderia, joka mm. värittää pikselit. Kun pikselit on väritetty, kuva siirretään eteenpäin esimerkiksi canvas-elementille näytettäväksi. Valitettavasti grafiikkaa ei voi luoda WebGL:n avulla ilman shareita.

Seuraavaan lähdekoodipätkään on kirjoitettu auki kolmion luontiin vaaditut vaiheet. Käytetyt shaderit ovat alempana.

// haetaan piirtoympäristö
var gl = $("#canvas")[0].getContext("experimental-webgl");

// luodaan puskuri kolmioille, ja kytketään se piirtokontekstin käyttöön
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// luodaan piirrettävä data. Piirretään kolmiota, joten luodaan
// kolme pistettä. Huom! Pisteiden sijainti riippuu katselupaikasta.
// x, y, z
var p1 = [1, 0, 0];
var p2 = [1, -1, 0];
var p3 = [-1, 1, 0];

var data = p1.concat(p2);
data = data.concat(p3);

// lisätään pisteet aiemmin luomaamme puskuriin
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);


// luodaan vertex shader
var vs = $("#vertex-shader").html();
var vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, vs);
gl.compileShader(vshader);
if (!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)) {
    throw gl.getShaderInfoLog(vshader);
}

// luodaan fragment shader
var fs = $("#fragment-shader").html();
var fshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fshader, fs);
gl.compileShader(fshader);
if (!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)) {
    throw gl.getShaderInfoLog(fshader);
}

// luodaan ohjelma ja liitetään shaderit siihen 
var program = gl.createProgram();
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    throw gl.getProgramInfoLog(program);
}

// otetaan ohjelma käyttöön ja annetaan attribuuttina dataa shadereille
gl.useProgram(program);
program.vertexPosAttrib = gl.getAttribLocation(program, "pos");
gl.enableVertexAttribArray(program.vertexPosAttrib);
gl.vertexAttribPointer(program.vertexPosAttrib, 2, gl.FLOAT, false, 0, 0);

// asetetaan taustaväri mustaksi ja tyhjennetään tausta
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

// piirretään kolmio
gl.drawArrays(gl.TRIANGLES, 0, 3);

Ohjelman käyttämät shaderit ovat yksinkertaisia. Vertex shader asettaa sijainnin, ja fragment shader värin.

    <script type="text/glsl" id="vertex-shader">
        attribute vec2 pos;
        void main() {
            gl_Position = vec4(pos, 0, 1);
        }
    </script>
    <script type="text/glsl" id="fragment-shader">
        void main() {
            gl_FragColor = vec4(1, 0, 0, 1);  // punainen väri
        }
    </script>

Näet ylläolevan ohjelman lopputuloksen klikkaamalla alla olevaa ruutua.

Three.js

WebGL on hyvin matalan tason kieli, ja yksinkertaistenkin ohjelmien rakentaminen puhtaasti sen avulla on hidasta ja usein itseään toistavaa. Ylläolevaa esimerkkiä olisi toiki voinut keventää luomalla omat funktiot shadereiden ja ohjelman alustukselle, mutta se on silti melko raskas. WebGL:lle on onneksi useita valmiita kirjastoja, jotka kapseloivat perustoiminnallisuuden. Eräs näistä kirjastoista on Three.js

Three.js tarjoaa valmiita komponentteja maailman luontiin (scene) tarkkailuun (kamera), sekä olioiden luomiseen (mm. mesh). Ylläolevan ohjelman voi luoda myös Three.js:n avulla seuraavasti:

var width = 600;
var height = 400;

// luodaan maailma
var scene = new THREE.Scene();

// luodaan kamera
var camera = new THREE.PerspectiveCamera( 40, width / height, 1, 1000 );
// lisätään kamera maailmaan
scene.add( camera );

// luodaan canvas-elementti, ja asetetaan sen koko
var renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );

// taustan väriksi tulee musta
renderer.setClearColorHex( 0x000000, 1 );


// lisää alue DOM-puuhun
document.body.appendChild( renderer.domElement );

// luodaan kolmio
var geom = new THREE.Geometry();
var v1 = new THREE.Vector3( 0,    1, 0);
var v2 = new THREE.Vector3(-1, -0.5, 0);
var v3 = new THREE.Vector3( 1,  0.5, 0);

geom.vertices.push(new THREE.Vertex(v1));
geom.vertices.push(new THREE.Vertex(v2));
geom.vertices.push(new THREE.Vertex(v3));

// määritellään sen pinta
geom.faces.push( new THREE.Face3( 0, 1, 2 ) );
geom.computeFaceNormals();

// ja asetetaan sille väri -- kolmio on punainen
var mesh = new THREE.Mesh( geom, new THREE.MeshBasicMaterial( { color: 0xff0000 } ));

// lisätään kolmio maailmaan
scene.add(mesh);

// siirrytään kameralla vähän taaksepäin niin nähdään luotu kolmio
camera.position.z = 3;

// ja näytetään maailma kameran näkökulmasta
renderer.render( scene, camera );

Ylläoleva esimerkki oikeastaan tekee paljon enemmän kuin WebGL-esimerkki, sillä se tarjoaa meille kameran, jonka avulla voimme tarkkailla maailmaa. Kameraan voi kytkeytyä aivan kuten mihin tahansa muuhunkin JavaScript-olioon. Voimme esimerkiksi lisätä näppäimistönkuuntelijan, joka mahdollistaa maailmassa liikkumisen näppäimistön avulla.

$(document).keydown(function(eventInfo) {
    // vasemmalle
    if(eventInfo.which === 37) {
        camera.position.x -= 0.1;
    }
    
    // oikealle
    if (eventInfo.which === 39) {
        camera.position.x += 0.1;
    }

    renderer.render( scene, camera );
});

Huomaa että kamera ei automaattisesti päivitä näkymäänsä sen siirtyessä, vaan meidän tulee kutsua uudelleen piirtokomentoa. Tässä vaiheessa kannattaisikin lähteä pohtimaan requestAnimationFrame-komennon käyttöä ja pelin rakennetta...

Laatikko

Harjoitellaan ihan hieman piirtämistä. Voit toki tehdä tässä tehtävässä myös enemmän. Tehtäväpohjassa oleva ohjelma luo punaisen kolmion mustalle taustalle. Kun käynnistät ohjelman WebGL:ää tukevassa ympäristössä näet allanäkyvän kuvan selainikkunassa.

Muokkaa ohjelmaa siten, että lisäät kuvaan vihreän kolmion. Lopputuloksen tulee näyttää seuraavalta.

Kun sovelluksesi toimii ja näet ylläolevan kuvan, palauta tehtävä TMC:lle. Huom! Jos olet varma että olet piirtänyt toisen kolmion oikein, mutta se ei näy, voit tarkistella kuvioiden ääriviivoja asettamalla Mesh-oliolle parametrin wireframe: true.

new THREE.Mesh( geom, new THREE.MeshBasicMaterial( {
            color: 0xff0000,
            wireframe: true
        } ));

Jos toisen kolmion ääriviivat näkyvät wireframe-parametrilla, kolmio on olemassa, mutta sen pinta osoittaa kamerasta poispäin. Muokkaa tällöin kolmion pisteiden järjestystä siten, että pinta osoittaa kameraan päin. Huomaa, että joudut poistamaan wireframe: true -parametrin, jotta näkisit kolmion konkreettisen värin.

Monisäikeisyys

Web Workerit ovat JavaScript-ohjelmia, joita voidaan suorittaa selaimen taustaprosesseina. Tämä mahdollistaa esimerkiksi suurempien laskuoperaatioiden toteuttamisen ilman että ne vaikuttavat käyttöliittymään. Käytännössä web workerit ovat erillisiä JavaScript-tiedostoja, jotka käynnistetään pääohjelmasta. Ne ovat erillisiä web-sivusta, eivätkä siten esimerkiksi pääse käsittelemään DOM-puuta. Web workereiden kanssa kommunikointi tapahtuu viestien avulla: niille lähetetään viestejä, esimerkiksi operaatioita, ja ne palauttavat viestejä: esimerkiksi operaation tilan tai lopputuloksen.

Tutkitaan pientä laskinta. Tiedostoon laskin.js on asetettu web worker-säikeeseen liittyvä lähdekoodi.

addEventListener("message", function(viesti) {
    var data = viesti.data;
    switch (data.operaatio) {
        case "summaa":
            summa(data.ekaluku, data.tokaluku);
            break;
    }
}, false);

function summa(eka, toka) {
    postMessage(eka+toka);
}

Kun ylläolevasta ohjelmasta luodaan worker-olio, se jää odottamaan viestejä. Worker-olion luonti tapahtuu kutsulla new Worker("lahdekooditiedosto.js");. Kun oliolle lähetetään viesti, se tutkii viestin sisällön (viesti.data). Viestiin liitetty olio on osana datan attribuutteja. Jos ohjelma löytää viestistä operaation "summaa", se kutsuu funktiota summa viestissä tulleilla parametreilla ekaluku ja tokaluku. Funktio summa laskee luvut, ja kutsuu workeriin liittyvää funktiota postMessage, joka lähettää viestin takaisin pääohjelmalle.

Tutkitaan seuraavaksi pääohjelmaa.

var laskin = new Worker("laskin.js");
            
laskin.addEventListener("message", function(message) {
    console.log("Viesti laskimelta: " + message.data);
}, false);
 
// kutsutaan laskimen operaatiota "summaa"
laskin.postMessage({
    operaatio: "summaa",
    ekaluku: 1,
    tokaluku: 5
});

Pääohjelma luo workerin "laskin", johon se lisää tapahtumankuuntelijan. Jos laskin lähettää pääohjelmalle viestin, suoritetaan console.log-komento, jolla tulos tulostetaan konsoliin. Kun tapahtumankuuntelija on lisätty, laskimelle lähetetään laskuoperaatio, joka sisältää operaation nimen ja kaksi lukua. Nämä laukaisevat laskimeen liittyvän tapahtumankuuntelijan, joka on määritelty tiedostossa laskin.js, jonka jälkeen laskuoperaatio suoritetaan erillisessä säikeessä.

Oleellista web workereissa on muistaa, että suoritus tapahtuu erillisissä säikeissä, ja kommunikointi tapahtuu viestien avulla.

Uncaught Error: SECURITY_ERR: DOM Exception 18

Jos näet ylläolevan virheviestin kun yrität ladata web workeria, tarkista että käytät web-developer -modea. Web workereiden käynnistämiseen liittyy samat CORS-tietoturvakysymykset kuin esimerkiksi AJAX-kutsujen tekemiseen.

 

SyntaxHighlighter (2p)

Tehtäväpohjassa tulee viime viikolta tuttu Typist-tehtävä. Tarkoituksenasi on tässä tehtävässä muuttaa ohjelmaa siten, että syntaksin väritys tapahtuu erillisessä Web Worker-säikeessä. Aloita refaktoroimalla syntaksin värityskoodi erilliseen JavaScript-tiedostoon, jonka pohjalta luot worker-olion.

Koska erilliset säikeet eivät pääse käsiksi DOM-puuhun tai muuhun sovelluksen dataan, tulee säikeelle antaa data parametrina. Aina kun sana vaihtuu, lähetä säikelle sanat ja värjättävän sanan indeksi. Kun säie vastaanottaa dataa, sen tulee käsitellä se, ja lopulta palauttaa syntaksinvärityksen sisältävä HTML-pätkä. HTML-koodi tulee asettaa näytölle aina kun se saadaan säikeeltä.

Kun syntaksin väritys tapahtuu erillisessä Web Workerissa ja sovelluksen toiminnallisuus on muuten ennallaan, palauta tehtävä TMC:lle.

Vaikka web workerit ovat hieno ominaisuus, kannattaa niiden käyttöönottoa pohtia kahdesti. Usein halutun laskennan voi siirtää palvelinpuolelle, joka pystyy hoitamaan laskuoperaation todennäköisesti paljon tehokkaammin. Tämä on erityisen tärkeää esimerkiksi mobiilisovelluksissa, sillä niissä tehon lisäksi myös akun määrä on aina rajoitettu. Toisaalta, jokaisen koneessa on huikeita määriä hukkatehoa, joka nykyään jää käyttämättä.

Ehkäpä jonkun pitäisi tehdä web-workereihin pohjautuva hyötysovellus, joka sivuja selatessa käyttää hukkatehoa esimerkiksi syöpägeenien analysointiin, vrt. http://www.cancergrid.org/...

Muutama sana tietoturvasta

OWASP (Open Web Application Security Project) listaa web-sovelluksiin liittyvät suurimmat tietoturvariskit. Vaikka web-sovelluksiin liittyvät tietoturvariskit ovat perinteisesti liittyneet palvelinpään toiminnallisuuden turvaamiseen, on selainohjelmistojen yleistyessä selainpuolen tietoturvan tärkeys yhä kasvavassa roolissa. Perinteiset riskit kuten cross-site scripting (XSS), missä käyttäjä saadaan suorittamaan jonkun toisen sivuille lisäämää lähdekoodia nousevat yhä uudestaan pintaan mashup-sivustoissa, jotka keräävät dataa useammasta palvelusta. Jos yhdessäkin palvelussa on ongelma, on mashup-sivustossa ongelma.

Yksi mielenkiintoisista hyökkäyksistä on cross-site request forgery, missä käyttäjä saadaan tekemään pyyntö erilliselle sivustolle. Jos käyttäjä on kirjautunut kohdesivustolle, ja kirjautumistiedot on tallennettu selaimen evästeisiin, ei kohdesivusto voi suoraviivaisesti erottaa normaalia pyyntöä "ilkeästä" pyynnöstä. Esimerkiksi eräs yhteisöpalvelu mahdollisti vielä jokunen vuosi sitten viestien lähettämisen suoran HTTP-apin kautta..

CSRF

Tutustu osoitteessa https://www.owasp.org/index.php/Top_10_2010-A5 olevaan CSRF kuvaukseen. Tässä tehtävässä pääset toteuttamaan oman hyökkäyksen.

Osoitteessa "http://aqueous-ravine-5531.herokuapp.com/app/login" on pankki, jonne käyttäjät voivat luoda uusia tunnuksia. Tässä tapauksessa salasana on aina "secret", ja käyttäjätunnus on tunnus, jota haluat käyttää. Kun uusi käyttäjä luodaan, hänelle lisätään 500000 euroa tilille (muutettu aiemmasta 1000 eurosta).

Pankilla on rajapinta rahan siirtämiseen, joka siirtää tällä hetkellä pankin sivuille kirjautuneelta käyttäjältä toiselle käyttäjätunnukselle rahaa. Rajapinta näyttää seuraavalta "http://aqueous-ravine-5531.herokuapp.com/app/transfer?to=kayttajatunnus&amount=1000", missä kayttajatunnus on käyttäjätunnus, jolle rahaa siirretään, ja amount on siirrettävä summa. Jotta rajapintaa voi käyttää, tulee käyttäjän olla kirjautunut. Pankissa on tarkistus, joka varmistaa että siirrettävä summa ei saa olla yli 1000 euroa, eikä alle 0 euroa.

Osoitteessa "http://t-avihavai.users.cs.helsinki.fi/lets/Chat" on chat-ohjelma, jossa on pieniä tietoturvaongelmia. Tehtävänäsi on toteuttaa CSRF-hyökkäys, jossa chatin käyttäjät siirtävät tilillesi rahaa chatatessään jos he ovat kirjautuneet pankkisivustolle. Huom! CRSF-hyökkäyksen ei tule yrittää kirjautumista. Hyödynnä tietoa siitä, että käyttäjä on mahdollisesti jo kirjautunut pankkiin!

Palauta tässä tehtävässä TMC:lle tehtäväpohjassa olevaan sivuun kirjattuna oma käyttäjätunnuksesi ja skripta/lähestymistapa, jolla sait rahaa tilillesi. Jos pankissa ei ole käyttöhetkellä useampia käyttäjiä, voit luoda itse niitä useampia.

Apuväline harjoitustyöhön: Game Manager (2p)

Kerrataan vielä hieman aiempia asioita ja rakennetaan pohja mahdollisen harjoitustyön tulosten tallentamiseen. Osoitteessa "http://aqueous-ravine-5531.herokuapp.com/app/games/" on REST-apia seuraava palvelu pelien tallentamiseen. Palvelun juuriosoitteeseen tehtävä GET-pyyntö hakee kaikki pelit listana. POST-pyyntö, jossa on pelin nimi luo uuden pelin. Pelillä on vain attribuutti name (POST-pyyntöä tehdessä, eli uutta oliota luodessa, id:n luominen tulee jättää palvelimen vastuulle -- älä lisää id:tä palvelimelle lähetettävään olioon.)

Rajapinta tarjoaa myös PUT- ja DELETE-operaatiot, jotka perustuvat pelin yksilöiviin tunnuksiin. Tunnukset luodaan POST-kyselyn yhteydessä. Luo palvelu, joka tarjoaa mahdollisuuden pelien lisäämiseen ja listaamiseen. Palvelun alkusivun tulee näyttää seuraavalta (tai hienommalta).

Kun käyttäjä klikkaa linkkiä "Add game", hänelle näytetään sivu pelien lisäämiseen. Kun pelien lisäyssivulla on täytetty pelin nimi, ja klikataan "Add", pelin tiedot lähetetään pelit tallentavaan palveluun.

List games-linkki taas tarjoaa listauksen tällä hetkellä olemassa olevista peleistä. Pelien lista noudetaan osoitteesta "http://aqueous-ravine-5531.herokuapp.com/app/games/".

Kun sivusi toimii kuten haluttu, palauta se TMC:lle. Voit käyttää valitsemaasi teknologiaa sivun toteuttamiseen.

Huom! Kun olet valmis, kokeile tehdä pyyntöjä palveluun siten, että lisäät polkuun pelin tunnuksen ja merkkijonon "scores". Näin pääset käsiksi tiettyyn peliin liittyviin tuloksiin. Esimerkiksi osoitteessa "http://aqueous-ravine-5531.herokuapp.com/app/games/1/scores/" on tunnuksella 1 tallennetun pelin tuloslistat. Tuloslistoille on myös REST-api, joka toimii aivan kuin pelien lisääminen. Voit käyttää tätä palvelua tulosten tallentamiseen harjoitustyössäsi.

Mihin emme ehtineet tutustua?

Kuusi viikkoa on loppujenlopuksi melko lyhyt aika. Kurssilla käytiin läpi selainohjelmoinnin perusteita JavaScriptillä, sekä tutustuttiin muutamaan sovelluskehykseen. Asioita, joihin kannattaa tutustua sitten kun on aikaa on huikeita määriä! Alla on muutamia linkkejä ja vinkkejä erilaisiin suuntiin.

JavaScript-ohjelmistojen testaaminen

Testaaminen on oleellinen osa ohjelmistokehitystä, jonka hyöty näkyy erityisesti sovelluksen ylläpitovaiheessa. Kun ohjelmistoa jatkokehitetään, jatkokehittäjät voivat olla täysin eri ihmisiä kuin sovelluksen alkuperäiset tekijät. Tällöin testit, jotka kertovat jos joku asia rikkoutui, ovat elintärkeitä. JavaScriptille löytyy useita testaussovelluskehyksiä, joista mainioita ovat muunmuassa QUnit ja Jasmine.

Lähdekooditiedostojen organisointi

Sovelluksia kehitettäessä lähdekooditiedostot asetetaan usein erillisiin pakkauksiin. Esimerkiksi Backbonessa on tapana luoda näkymät omaan kansioon, modelit omaan kansioon, ja sovellus omaan kansioon. Tiedostojen latausjärjestyksellä on usein myös väliä. Latausta helpottamiseen on kehitetty muutamia työkaluja, joista tärkein on RequireJS.

Käytettävyys, entä jos JavaScriptiä ei tueta?

Käyttöliittymien käytettävyys on täysin oma maailmansa. Käyttöliittymän tehokkuus, miellyttävyys, ja muistamisen helppous vaikuttavat erittäin paljon käyttäjän selauskokemukseen. Osa webin käyttjistä käyttää erilaisia ruudunlukuohjelmia, ja osa käyttäjistä jättää JavaScriptin kokonaan pois päältä. Kannattaa tutustua WebAIM-projektin maaliskuussa 2012 teettämään kyselyyn, joka antaa kuvaa pienempien käyttäjäryhmien erilaisista rajoitteista.

Uudet selainteknologiat ovat yleisesti ottaen huikeita, mutta mm. erilaisissa työpaikkaympäristöissä käyttäjät eivät pääse automaattisesti käyttämään uudempia ohjelmistoja, jotkut organisaatiot kieltävät JavaScriptin käytön tietoturvasyistä, ja osa työpaikkojen käyttämistä järjestelmistä on auttamatta kehityksestä jäljessä. Tärkeintä on ottaa selville oma kohdeyleisö, ja pyrkiä tukemaan heitä mahdollisimman hyvin. Projektit kuten JQuery pyrkivät luomaan JavaScriptin ympärille tuen, joka toimii mahdollisimman useassa ympäristössä. Toisaalta, lopullista selauskokemusta pohdittaessa tulee miettiä miten sivun tulisi käyttäytyä jos joku komponentti ei ole olemassa. Modernizr tarjoaa tuen selaintuen tarkistamiseen.

Miljoona muuta sovelluskehystä ;)

Selainteknologiat elävät tällä hetkellä erittäin nopeaa kehitystä, ja erilaisia sovelluskehyksiä syntyy nopeaa tahtia. Suurin osa kehyksistä seuraa kurssillamme kuljettuja viivoja ja tarjoavat Mustachen kaltaisen templatetuen sekä backbonen kaltaisen model-tuen. Tämän materiaalin kirjoitushetkellä mielenkiintoisia kehyksiä ovat muunmuassa AngularJS ja Ember.js.

Mitä seuraavaksi...

Harjoitustyö

Kurssiin kuuluu vapaaehtoinen 2 opintopisteen harjoitustyö. Lisää tietoa harjoitustyöstä löydät osoitteesta https://docs.google.com/document/d/12o5SmUYoCg8TKwSXhI4z0xY6-KLvI--j4EQKvUo56ZY/view.

Koe

Kurssin koe järjestetään tiistaina 11.12.2012 klo 09:00 saleissa A111 ja B123. Kokeessa ei tarvitse osata esimerkiksi grafiikkaohjelmointiin liittyvää syntaksia. Tärkeämpää on ymmärtää selainohjelmointiin liittyvät perusajatukset kuten DOM-puu, JavaScriptin käyttö, sekä palvelimen kanssa kommunikointi.

Millaisia koekysymykset esimerkiksi ovat? .. vikalla luennolla

Kurssipalaute

Käy antamassa kurssipalautetta osoitteessa https://ilmo.cs.helsinki.fi/kurssit/servlet/Valinta. Kerro erityisesti mitä olisit kaivannut enemmän, mitä vähemmän, ja mihin tulisi kiinnittää (mahdollisella) seuraavalla kerralla enemmän huomiota! Muutamia mainioita ideoita onkin tullut jo tehtävien mukana tulleissa palautteissa sekä pajassa, kiitos jo nyt teille :).

Kun olet antanut palautetta, lähetä tämä tehtävä TMC:lle. Voit myös palauttaa tehtävän nyt ja olla antamatta palautetta sillä ehdolla, että annat palautetta kurssikokeen jälkeen.