Lomakkeet
Käsitellään HTML-lomakkeiden luomista, lomakkeiden ohjelmallista käsittelyä ja lomakekenttien syötteiden rajoitteita eli tarkistuksia. Jos lomakkeet eivät ole tuttuja, katso ensin WWW-lomakkeet-materiaali ja Basic native form controls.
Huomio XHTML:stä
Lomakkeissa on paljon totuusarvoattribuutteja. HTML-koodissa riittää pelkkä attribuutin nimi, mutta XML:ssä ei ole totuusarvoattribuutteja, joten kurssin XHTML-sivuilla ne kirjoitetaan muodossa nimi="nimi":
<input type="text" name="nimi" required="required" />
<input type="checkbox" name="ehdot" checked="checked" />
<select name="maat" multiple="multiple"></select>
<form novalidate="novalidate"></form>
Tämä koskee attribuutteja required, checked, disabled, readonly, multiple, selected, autofocus, novalidate ja formnovalidate. Muista myös sulkea tyhjät elementit: <input ... />, ja ei-tyhjät elementit kokonaan: <option value="1"></option>.
form
Lomake-elementtejä voi käyttää myös ilman form-elementtiä, mutta yleensä ne on syytä sijoittaa sen sisään. Ilman lomaketta et saa käyttöösi submit-tapahtumaa, automaattista validointia, FormData-objektia etkä form.elements-kokoelmaa.
form-elementin html-attribuutit
- action lomakkeen käsittelevän ohjelman URI-osoite. Ei käytetä tällä kurssilla, koska lomake käsitellään selaimessa.
- method määrää HTTP-metodin, jolla lomakkeen tiedot lähetetään. Ei tarvita tällä kurssilla.
- enctype määrää lomakkeen lähetystietojen MIME-tyypin. Ei tarvita tällä kurssilla.
- autocomplete määrää, voiko selain täyttää lomakkeen sisältöä automaattisesti aiemmin syötetyillä arvoilla (on tai off).
- novalidate estää lomakkeen automaattisen validoinnin lähetyksen yhteydessä. Huomaa, että se estää vain interaktiivisen validoinnin: kenttien validity-ominaisuus ja CSS:n :invalid-valitsin toimivat edelleen, ja validoinnin voi tehdä itse checkValidity()-metodilla.
Lomake-elementit
Yleisimmät lomakkeissa käytetyt elementit on käsitelty ITKP1011-kurssin WWW-lomakkeet-luennolla. Tässä kerrataan tämän kurssin kannalta olennaiset.
label on pakollinen
Jokaisella lomakekentällä on oltava label, jossa on selittävä teksti. Ilman sitä ruudunlukija ei kerro käyttäjälle mitä kenttään pitäisi kirjoittaa, eikä kentän klikkausalue laajene tekstiin.
<!-- kääritty muoto: label sisältää sekä tekstin että kentän -->
<label>Nimi <input type="text" name="nimi" /></label>
<!-- for- ja id-attribuuteilla yhdistetty muoto -->
<label for="nimi">Nimi</label>
<input type="text" name="nimi" id="nimi" />
placeholder ei korvaa labelia. Se katoaa heti kun käyttäjä alkaa kirjoittaa, sen kontrasti on selaimissa heikko, eikä se kelpaa kentän nimeksi. Käytä sitä korkeintaan esimerkkinä muodosta (placeholder="esim. 040 1234567").
autofocus-attribuuttia kannattaa välttää. Se siirtää kohdistuksen sivun latauduttua, mikä hämmentää ruudunlukijan käyttäjää ja voi vierittää sivua odottamattomasti.
input-elementin tyypit
input-elementin tärkein attribuutti on type, joka määrää millainen kenttä on kyseessä. Oikean tyypin valinta on tärkeää: selain antaa tyypin mukaisen näppäimistön mobiililaitteessa ja tekee tyyppikohtaisen validoinnin ilmaiseksi.
- text vapaamuotoinen tekstikenttä
- password kuten tekstikenttä, mutta merkit näkyvät pisteinä
- checkbox valintaruutu, monta valittavissa
- radio valintanappi, yksi valittavissa ryhmästä
- submit lomakkeen lähetyspainike
- reset lomakkeen tyhjentävä painike
- button tavallinen painike, joka ei lähetä lomaketta
- file tiedoston valinta
- hidden piilotettu tieto
- color värivalitsin
- date, time, datetime-local, month, week päivämäärä- ja aikakentät
- email, tel, url yhteystietokentät
- number numero, range liukusäädin
- search hakukenttä
input-elementin tärkeimmät attribuutit
- name kentän nimi. Ilman tätä kenttä ei lähde lomakkeen mukana eikä löydy form.elements-kokoelmasta.
- value kentän alkuarvo
- required määrää kentän pakolliseksi
- min, max kentän pienin ja suurin sallittu arvo
- minlength, maxlength merkkien vähimmäis- ja enimmäismäärä
- step sallittu askel. Jos min="0", max="100" ja step="10", kelvollisia arvoja ovat 0, 10, 20 ja niin edelleen.
- pattern säännöllinen lauseke, jota vastaan syöte tarkistetaan
- placeholder vinkki syötteen muodosta
- readonly arvoa ei voi muuttaa, mutta se lähtee lomakkeen mukana
- disabled kenttä ei ole käytettävissä eikä se lähde lomakkeen mukana
- checked radio- tai checkbox-kentän alkutila
- multiple useampi arvo sallittu. Koskee vain email- ja file-kenttiä.
- list viittaa datalist-elementtiin
- autocomplete kertoo selaimelle, mitä tietoa kenttään syötetään (esim. "email", "street-address")
- form kertoo mihin lomakkeeseen kenttä kuuluu, jos se on lomakkeen ulkopuolella
Huomaa ero readonly- ja disabled-attribuuttien välillä. Molemmat estävät muokkaamisen, mutta vain readonly-kentän arvo lähtee lomakkeen mukana. disabled-kenttiä ei myöskään validoida.
Dynaaminen lomake, joka lisää ja poistaa syöttökenttiä tarpeen mukaan:
datalist
Sisältää option-elementtejä. Datalistiin viitataan input-elementin list-attribuutilla, jolloin sen sisältöä ehdotetaan kentän arvoksi. Toisin kuin select-elementissä, käyttäjä voi kirjoittaa myös jotain muuta — jos vain listan arvot kelpaavat, käytä select-elementtiä.
<label>Luku
<input name="luvut" type="text" value="30" list="lukuja"
pattern="10|20|30|40" />
</label>
<datalist id="lukuja">
<option value="10"></option>
<option value="20"></option>
<option value="30"></option>
<option value="40"></option>
</datalist>
meter
Havainnollistaa mittalukemaa tunnetulla asteikolla: levytilan käyttöä, äänenvoimakkuutta, uunin lämpötilaa. Arvo voi liikkua kumpaankin suuntaan.
Lämmitä uuni <meter min="100" max="250" value="200">200 asteeseen</meter>
progress
Esittää tehtävän etenemistä, joka kulkee alusta loppuun. Ero meter-elementtiin on juuri tässä: latauspalkki on progress, levytilamittari on meter. Ilman value-attribuuttia palkki on määrittelemättömässä tilassa.
<progress value="70" max="100">70 %</progress>
<progress>Ladataan…</progress>
output
Esittää laskennan tai toiminnan tuloksen. for-attribuutti kertoo, mistä kentistä tulos on laskettu. Arvo asetetaan value-ominaisuudella.
<label>Luku 1 <input type="number" name="luku1" id="luku1" value="10" /></label>
<label>Luku 2 <input type="number" name="luku2" id="luku2" value="20" /></label>
<output name="summa" for="luku1 luku2">30</output>
const lomake = document.forms.laskuri;
lomake.addEventListener("input", function () {
const a = lomake.elements.luku1.valueAsNumber;
const b = lomake.elements.luku2.valueAsNumber;
lomake.elements.summa.value = a + b;
});
output-elementillä on aria-live-käyttäytyminen sisään rakennettuna, joten ruudunlukija kertoo muuttuneen arvon automaattisesti.
DOM-rajapinta
Lomakkeen ja kenttien hakeminen
Sivun kaikkiin lomakkeisiin pääsee käsiksi document.forms-kokoelman kautta. Se on elävä HTMLCollection, joten se heijastaa sivuun tehdyt muutokset.
document.forms[0] // sivun ensimmäinen lomake
document.forms.lomake // lomake, jonka name- tai id-attribuutti on "lomake"
document.forms["lomake"] // sama asia
Lomakkeen kenttiin pääsee käsiksi lomakkeen kautta, ei suoraan document.forms-kokoelmasta:
<form id="lomake">
<label>Eka <input name="eka" type="text" value="1" /></label>
<label>Toka <input name="toka" type="text" value="2" /></label>
</form>
const lomake = document.forms.lomake;
lomake.elements.eka // input-elementti, selkein muoto
lomake.elements[0] // sama indeksillä
lomake.eka // toimii myös, mutta katso varoitus alla
// document.forms.eka EI TOIMI: document.forms sisältää vain
// form-elementtejä, tulos on undefined
Käytä form.elements-muotoa. Lyhyempi form.eka toimii, mutta se törmää lomakkeen omiin ominaisuuksiin ja metodeihin. Jos kentän nimeksi antaa vaikkapa reportValidity, niin
lomake.reportValidity(); // TypeError: ei ole funktio
— koska nimi viittaakin nyt input-elementtiin eikä metodiin. Älä siis käytä kenttien niminä olemassa olevien ominaisuuksien tai metodien nimiä, ja hae kentät elements-kokoelman kautta.
FormData
Kun koko lomakkeen sisältö halutaan kerralla, FormData on siisteintä. Se kerää kaikki nimetyt kentät, jotka eivät ole disabled-tilassa.
const data = new FormData(lomake);
data.get("nimi"); // yhden kentän arvo
data.getAll("harrastukset"); // kaikki samannimiset (checkboxit)
for (const [avain, arvo] of data) {
console.log(`${avain} = ${arvo}`);
}
// koko lomake tavallisena objektina
const arvot = Object.fromEntries(data); // { nimi: "Oona", ika: "21" }
Huomaa, että kaikki arvot ovat merkkijonoja, myös numerokenttien. Muunna ne Number()-funktiolla tai lue arvo suoraan kentän valueAsNumber-ominaisuudesta. Object.fromEntries hukkaa samannimisten kenttien arvoista kaikki paitsi viimeisen, joten checkbox-ryhmiin käytä getAll-metodia.
Lomakkeiden rajapintaluokat
- HTMLFormElement
- HTMLInputElement
- HTMLSelectElement
- HTMLTextAreaElement
- HTMLOptionElement
- HTMLOptGroupElement
- HTMLButtonElement
- HTMLDataListElement
- HTMLLabelElement
- HTMLFieldSetElement
- HTMLLegendElement
- HTMLMeterElement
- HTMLProgressElement
- HTMLOutputElement
- RadioNodeList
- ValidityState
- FormData
HTMLInputElement
Useimmat ominaisuudet viittaavat suoraan vastaaviin HTML-attribuutteihin. Huomaa kirjoitusasu: attribuutti maxlength, ominaisuus maxLength.
- value kentän nykyinen arvo merkkijonona
- defaultValue kentän alkuarvo eli value-attribuutin arvo
- valueAsNumber arvo numerona. NaN, jos arvoa ei voi tulkita luvuksi.
- valueAsDate arvo Date-objektina päivämääräkentissä
- checked ja defaultChecked, katso alla
- name, type, form
- disabled, readOnly, required, autofocus
- min, max, step, minLength, maxLength, pattern, placeholder, size, multiple, list, autocomplete
- validity ja validationMessage, katso Validointi
Tärkeimpiä metodeja:
- focus(), blur(), click()
- select() valitsee kentän koko sisällön
- stepUp() ja stepDown()
- setCustomValidity(), checkValidity() ja reportValidity(), katso Validointi
radio ja checkbox
Valintanapit ja valintaruudut ovat erikoistapaus input-elementistä. Näistä ominaisuuksista on syytä muistaa:
- checked lukee tai asettaa nykyisen valinnan. Arvo on true tai false: input.checked = true;
- defaultChecked lukee tai asettaa alkuarvon eli sen, mitä HTML-koodissa lukee. Huomaa iso C. Vastaava attribuutti on checked, ja getAttribute("checked") kertoo vain onko attribuutti olemassa — se ei kerro kentän nykyistä tilaa.
- indeterminate asettaa valintaruudun epämääräiseen tilaan, jossa se ei ole valittu eikä valitsematta. Tila näkyy vain ulkoasussa eikä vaikuta checked-arvoon. Tätä tarvitaan harvoin.
Valitun valintanapin arvon saa suoraan ryhmän nimellä. Kun useammalla radiolla on sama name, form.elements palauttaa RadioNodeList-kokoelman, jonka value on valitun napin arvo. Silmukkaa ei tarvita:
<fieldset>
<legend>Koko</legend>
<label><input type="radio" name="koko" value="S" /> Pieni</label>
<label><input type="radio" name="koko" value="M" /> Keskikoko</label>
<label><input type="radio" name="koko" value="L" /> Suuri</label>
</fieldset>
lomake.elements.koko.value; // "M" — valitun napin arvo
lomake.elements.koko.value = "L"; // valitsee L-vaihtoehdon
// jos mitään ei ole valittu, arvo on tyhjä merkkijono
if (lomake.elements.koko.value === "") {
console.log("valinta puuttuu");
}
Valintaruuduilla value ei kelpaa sellaisenaan, koska niitä voi olla valittuna useita. Samannimiset kentät saa silti kerralla form.elements-kokoelmasta, ja valitut poimitaan siitä checked-ominaisuuden perusteella:
const ryhma = lomake.elements.harrastukset; // RadioNodeList
const valitut = [...ryhma]
.filter(kentta => kentta.checked)
.map(kentta => kentta.value);
Varo yhden alkion tapausta. Jos kyseisellä nimellä on vain yksi kenttä, form.elements.harrastukset palauttaa kokoelman sijaan itse elementin, eikä sitä voi levittää taulukoksi. Jos määrä voi vaihdella, käytä FormData-objektin getAll-metodia, joka toimii kummassakin tapauksessa:
const valitut = new FormData(lomake).getAll("harrastukset");
Kts. checked.html
HTMLSelectElement
- value valitun vaihtoehdon arvo. Tämä on se, jota useimmiten tarvitset. Jos mitään ei ole valittu, arvo on tyhjä merkkijono.
- selectedIndex valitun vaihtoehdon järjestysnumero, tai -1 jos mitään ei ole valittu
- options kaikki option-elementit
- selectedOptions valitut option-elementit
- multiple, size
- type arvona "select-one" tai "select-multiple"
valikko.value; // valitun arvo
valikko.selectedOptions[0].textContent; // valitun näkyvä teksti
// monivalinnassa value antaa vain ensimmäisen valitun
const kaikki = [...valikko.selectedOptions].map(o => o.value);
Vaihtoehtoja voi lisätä ja poistaa options-kokoelman kautta tai tavallisilla DOM-metodeilla. Poistamiseen on myös remove()-metodi.
const vaihtoehto = document.createElement("option");
vaihtoehto.value = "fi";
vaihtoehto.textContent = "Suomi";
valikko.append(vaihtoehto);
HTMLTextAreaElement
Monirivisen tekstikentän tärkeimmät ominaisuudet:
- value kentän nykyinen sisältö. Älä käytä textContent-ominaisuutta — se koskee vain elementin alkuperäistä sisältöä eikä muutu käyttäjän kirjoittaessa.
- defaultValue alkuarvo eli se, mitä elementin sisällä lukee HTML-koodissa
- textLength sisällön pituus merkkeinä
- rows ja cols kentän näkyvä koko riveinä ja merkkeinä
- wrap rivittyykö teksti ("soft" tai "hard")
- minLength ja maxLength merkkien vähimmäis- ja enimmäismäärä
- required, readOnly, disabled, placeholder, autofocus, name, form
- selectionStart, selectionEnd valinnan alku ja loppu
- validity ja validationMessage
Metodeja: select(), setSelectionRange(), setRangeText(), focus(), blur(), setCustomValidity(), checkValidity() ja reportValidity().
const kentta = lomake.elements.kuvaus;
kentta.value; // nykyinen sisältö
kentta.defaultValue; // alkuperäinen sisältö
kentta.textLength; // merkkien määrä
kentta.value = ""; // tyhjennys
HTMLFormElement
Tärkeimmät ominaisuudet:
- elements lomakkeen kaikki kentät. Tämän kautta kentät haetaan. Kokoelmasta voi hakea sekä nimellä että indeksillä.
- length kenttien lukumäärä
- name lomakkeen nimi
- noValidate vastaa novalidate-attribuuttia. Huomaa iso V.
- autocomplete saako selain täyttää lomakkeen automaattisesti
- action, method, enctype, acceptCharset lomakkeen lähetykseen liittyvät. Näitä ei tarvita tällä kurssilla, koska lomake käsitellään selaimessa.
Metodeja:
- checkValidity() tarkistaa kaikki kentät, palauttaa totuusarvon
- reportValidity() sama, mutta näyttää virheilmoitukset käyttäjälle
- reset() palauttaa kentät alkuarvoihinsa
- requestSubmit() lähettää lomakkeen kuten käyttäjän painallus: validointi tehdään ja submit-tapahtuma laukeaa
- submit() lähettää lomakkeen ohittaen validoinnin ja submit-tapahtuman
Ero submit()- ja requestSubmit()-metodien välillä on merkittävä. Jos lomake lähetetään koodista submit()-metodilla, kentät jäävät tarkistamatta eikä oma submit-käsittelijä suoriudu lainkaan — mikä on lähes aina yllätys. Käytä requestSubmit()-metodia.
Validointi
Käytä aina ensisijaisesti HTML-koodissa määriteltyjä rajoitteita, jos ne riittävät tarkoitukseesi. Selain hoitaa tarkistuksen, virheilmoituksen ja sen kääntämisen käyttäjän kielelle ilmaiseksi.
HTML-rajoitteet
Rajoitteita ovat required, min, max, step, minlength, maxlength, pattern sekä kentän type (esimerkiksi type="email" tarkistaa osoitteen muodon).
pattern on aina ankkuroitu. Lauseke tarkistetaan koko arvoa vasten, ikään kuin sen ympärillä olisi ^ ja $. Tämä poikkeaa tavallisesta RegExp-käytöksestä. Anna kentälle myös title-attribuutti, jonka teksti näkyy selaimen virheilmoituksessa:
<label>Postinumero
<input type="text" name="postinumero" pattern="[0-9]{5}"
title="Viisi numeroa, esimerkiksi 40100" required="required" />
</label>
Huom. minlength- ja maxlength-rajoitteisiin liittyvä validiteetti päivittyy vain käyttäjän tekemien muutosten yhteydessä. Jos muutat kentän value-ominaisuutta koodista, se ei vaikuta näihin tarkistuksiin.
ValidityState
Jokaisella kentällä on validity-ominaisuus, josta näkee onko kenttä validi ja mitä rajoitetta se rikkoo. Sen lukeminen ei laukaise mitään tapahtumia, joten se sopii hiljaiseen tarkistukseen.
kentta.validity.valid; // onko kenttä kelvollinen
kentta.validity.valueMissing; // pakollinen kenttä on tyhjä
kentta.validity.typeMismatch; // esim. type="email" ja väärä muoto
kentta.validity.patternMismatch; // pattern ei täsmää
kentta.validity.rangeUnderflow; // alle min-arvon
kentta.validity.rangeOverflow; // yli max-arvon
kentta.validity.tooShort; // alle minlength
kentta.validity.tooLong; // yli maxlength
kentta.validity.stepMismatch; // ei osu step-askeleeseen
kentta.validity.customError; // setCustomValidity on asetettu
kentta.validationMessage; // selaimen virheilmoitus tekstinä
Omat tarkistukset
Oman virheilmoituksen asettaa setCustomValidity()-metodi. Jos viesti on muuta kuin tyhjä merkkijono, kenttä on epävalidi. Virheilmoitus on siis muistettava tyhjentää, kun virhettä ei enää ole:
function tarkistaSalasana(e) {
const kentta = e.currentTarget;
if (kentta.value.length >= 8 && /[0-9]/.test(kentta.value)) {
kentta.setCustomValidity(""); // ei virhettä
}
else {
kentta.setCustomValidity("Salasanassa on oltava vähintään 8 merkkiä ja numero.");
}
kentta.reportValidity(); // näytä ilmoitus käyttäjälle
}
lomake.elements.salasana.addEventListener("input", tarkistaSalasana);
checkValidity, reportValidity ja invalid
| Tapa | Palauttaa | Näyttää ilmoituksen | Laukaisee invalid-tapahtuman |
|---|---|---|---|
| validity.valid | totuusarvon | ei | ei |
| checkValidity() | totuusarvon | ei | kyllä |
| reportValidity() | totuusarvon | kyllä | kyllä |
Molemmat metodit toimivat sekä yksittäiselle kentälle että koko lomakkeelle. Jos haluat vain tietää tilanteen aiheuttamatta mitään, lue validity.valid-ominaisuus.
invalid-tapahtuma laukeaa epävalidille kentälle, kun lomaketta yritetään lähettää tai kun kutsutaan checkValidity()- tai reportValidity()-metodia. invalid ei kupli, joten sitä ei voi delegoida lomakkeelle tavalliseen tapaan — käsittelijä on lisättävä jokaiseen kenttään tai käytettävä capture-vaihetta:
// tämä EI toimi, koska invalid ei kupli
lomake.addEventListener("invalid", kasittelija);
// capture-vaiheessa toimii
lomake.addEventListener("invalid", kasittelija, { capture: true });
Milloin validointi tapahtuu
Jos lomakkeella ei ole novalidate-attribuuttia, kentät validoidaan automaattisesti lomaketta lähetettäessä. Nämä painikkeet lähettävät lomakkeen:
<button>Painike 1</button>
<button type="submit">Painike 2</button>
<input type="submit" value="Painike 3" />
Nämä eivät lähetä lomaketta, vaan niistä laukeaa pelkkä click-tapahtuma:
<button type="button">Pelkkä click-painike</button>
<input type="button" value="Pelkkä click-painike" />
submit on lomakkeen tapahtuma, ei painikkeen. Kuuntelija on siis liitettävä form-elementtiin, ei lähetyspainikkeeseen. Painikkeessa submit-tapahtumaa ei koskaan tapahdu, vaikka sen kuuntelijan sinne lisäisikin:
lomake.addEventListener("submit", omasubmit); // OIKEIN
nappi.addEventListener("submit", omasubmit); // ei laukea koskaan
Tämä on myös syy siihen, miksi lomaketta ei kannata käsitellä painikkeen click-tapahtumassa: lomakkeen voi lähettää myös painamalla Enteriä tekstikentässä, jolloin painiketta ei klikata lainkaan, mutta submit-tapahtuma laukeaa normaalisti.
Lähetyspainikkeesta laukeaa ensin click-tapahtuma, ja lomakkeen submit-tapahtuma vasta sen jälkeen jos kaikki kentät ovat valideja. Poikkeuksia on kaksi: submit-tapahtumaa ei tapahdu, jos click-käsittelijässä kutsutaan preventDefault()-metodia, ja se tapahtuu aina, jos lomakkeella on novalidate-attribuutti.
formnovalidate on lähetyspainikkeen attribuutti, ei kentän. Sillä varustettu painike ohittaa koko lomakkeen validoinnin — tyypillinen käyttö on "tallenna luonnos" -painike:
<button type="submit" formnovalidate="formnovalidate">Tallenna luonnos</button>
Yksittäisen kentän validoinnin poistamiseen ei ole attribuuttia. Jos kenttää ei haluta validoida, siltä poistetaan rajoitteet tai se asetetaan disabled-tilaan.
Validointi heti kentän täyttämisen yhteydessä
- Tarkista kentän syöte input- tai change-tapahtumassa. input laukeaa jokaisen merkin jälkeen, change vasta kun muokkaus päättyy.
- Tee omat javascript-tarkistukset. Rajoitteiden tilan voi lukea validity-ominaisuudesta ilman sivuvaikutuksia.
- Aseta oma virheilmoitus setCustomValidity()-metodilla, tai tyhjennä se tyhjällä merkkijonolla jos virhettä ei ole.
- Kutsu kentän reportValidity()-metodia, joka näyttää käyttäjälle sekä oman että HTML-rajoitteiden virheilmoitukset.
- Muokkaa virheellisten kenttien ulkoasua CSS:llä, katso alla.
Lomake hyväksytään ja submit-tapahtuma sallitaan vasta, kun kaikissa kentissä on validi sisältö.
Koko lomakkeen validointi täyttämisen jälkeen
Kaikkia virheitä ei pysty tarkistamaan heti kentän täyttämisen yhteydessä — esimerkiksi kahden kentän keskinäistä suhdetta ei voi tarkistaa ennen kuin molemmat on täytetty. Tällöin tarkistus tehdään vasta lopuksi.
Tässä on yksi sudenkuoppa: submit-tapahtumassa ei voi poistaa omia virheilmoituksia, koska tapahtuma ei laukea silloin kun lomakkeella on virheitä. Vaihtoehtoja on kaksi:
- Aseta lomakkeelle novalidate-attribuutti, jolloin submit-tapahtuma laukeaa aina ja voit tehdä koko tarkistuksen siinä. Tämä on siistein tapa.
- Tee tarkistukset lähetyspainikkeen click-tapahtumassa, joka tapahtuu aina.
// lomakkeella on novalidate="novalidate"
lomake.addEventListener("submit", function (e) {
e.preventDefault();
const alku = lomake.elements.alkupvm;
const loppu = lomake.elements.loppupvm;
if (alku.value !== "" && loppu.value !== "" && alku.value > loppu.value) {
loppu.setCustomValidity("Loppupäivä ei voi olla ennen alkupäivää.");
}
else {
loppu.setCustomValidity("");
}
if (!lomake.reportValidity()) {
return; // virheitä: ilmoitukset näytetty, ei jatketa
}
kasitteleLomake(new FormData(lomake));
});
Virheellisten kenttien ulkoasu
CSS:ssä on valmiit valitsimet kelvollisille ja kelvottomille kentille. Käytä :user-invalid- ja :user-valid-valitsimia eikä :invalid- ja :valid-valitsimia.
Ero on käytännössä ratkaiseva: :invalid osuu tyhjään pakolliseen kenttään heti sivun latauduttua, ennen kuin käyttäjä on ehtinyt tehdä mitään. Lomake näyttää siis punaiselta ennen kuin siihen on koskettu. :user-invalid osuu vasta kun käyttäjä on ollut kentässä ja poistunut siitä.
input:user-invalid {
border-color: #b3261e;
background-color: #fdeceb;
}
input:user-valid {
border-color: #2e7d4f;
}
/* pakolliset kentät merkitään myös näkyvästi */
label:has(input:required)::after {
content: " *";
color: #b3261e;
}
Älä luota pelkkään väriin: värisokea käyttäjä ei erota punaista reunaa vihreästä. Virheilmoituksen teksti on aina näytettävä.
Selainerot
Selaimet esittävät virheilmoitukset eri tavoin, ja käyttäytyminen muuttuu versioittain. Erityisesti oman setCustomValidity()-viestin näkyminen ilman reportValidity()-kutsua vaihtelee selaimittain. Testaa siis aina sekä Firefoxilla että Chromella, äläkä rakenna logiikkaa sen varaan, että ilmoitus näkyisi ilman erillistä reportValidity()-kutsua.
Jos haluat täyden hallinnan ulkoasuun, ota selaimen ilmoitus pois käytöstä novalidate-attribuutilla ja näytä virheet itse sivun omissa elementeissä. Muista silloin liittää virheilmoitus kenttään aria-describedby-attribuutilla.
Esimerkki 2 (lomake.js) esittelee click- ja submit-tapahtumien toimintaa eri tilanteissa. Sama esimerkki interaktiivisena — suorita run-painikkeella:
Saavutettavuus ja ARIA
Saavutettavuuden perusteet käydään läpi ITKP1011-kurssin luennolla Saavutettavuus ja käytettävyys: WCAG:n neljä periaatetta, lainsäädäntö, kontrastit, vaihtoehtoiset tekstit, otsikkotasot, linkkitekstit, hyppylinkki ja sivun testaaminen ilman kuvia, ilman tyylejä, näppäimistöllä ja ruudunlukijalla. Tämä luku ei toista niitä.
Tässä käsitellään sitä, mikä tulee mukaan kun sivu alkaa muuttua javascriptin vaikutuksesta. Staattisen sivun saa saavutettavaksi oikealla merkkauksella, mutta heti kun ohjelma muuttaa sivua, saavutettavuus siirtyy ohjelmoijan vastuulle: fokus ei siirry itsestään, eikä ruudunlukija huomaa muutosta jota se ei ole itse aiheuttanut.
| ITKP1011 | Tämä luku |
|---|---|
| WCAG:n periaatteet ja lainsäädäntö | — |
| oikeat elementit ja rakenne | elementin tila, kun se muuttuu |
| vaihtoehtoiset tekstit ja kontrastit | ohjelmallisesti asetetut nimet |
| ARIAn ensimmäinen sääntö | mitä ARIA on ja miten se toimii |
| sarkaimella läpi, ei jumiin | fokuksen siirtäminen ohjelmallisesti |
| staattisen sivun testaaminen | muuttuvan sivun ja ilmoitusten testaaminen |
Mikä ARIA on
ARIA eli Accessible Rich Internet Applications on W3C:n määrittely, joka lisää merkkaukseen tietoa elementin roolista, tilasta ja ominaisuuksista. Käytännössä se on joukko role- ja aria--alkuisia attribuutteja.
Selain rakentaa sivusta DOM-puun rinnalle saavutettavuuspuun (accessibility tree). Se on karsittu esitys, joka kertoo apuvälineille mitä sivulla on: tässä on painike, tässä on otsikkotason 2 otsikko, tämä valintaruutu on valittuna. Ruudunlukija ei lue merkkausta vaan tätä puuta.
Saavutettavuuspuu johdetaan merkkauksesta automaattisesti, ja oikeat elementit tuottavat sen oikein ilman eri toimenpiteitä. ARIAa tarvitaan kahdessa tilanteessa.
1. HTML:stä puuttuu elementti. Välilehdille, puunäkymälle, yhdistelmäkentälle tai ilmoituslistalle ei ole omaa elementtiä. Ilman ARIAa niistä tulee saavutettavuuspuussa nimettömiä div-elementtejä.
2. Tila muuttuu ajon aikana. Kun valikko avautuu, rivi valitaan tai lomakkeen lähetys epäonnistuu, muutos näkyy silmällä mutta saavutettavuuspuussa ei mitään tapahtunut. ARIA on tapa kertoa siitä.
Mitä ARIA ei tee
Tämä on tärkeintä ymmärtää, ja se menee usein väärin. ARIA ei muuta elementin toimintaa eikä ulkoasua. Se muuttaa vain sitä, mitä apuvälineelle kerrotaan.
<!-- Tämä EI ole painike. Se näyttää ruudunlukijalle painikkeelta,
mutta ei saa fokusta eikä reagoi välilyöntiin tai enteriin. -->
<div role="button">Tallenna</div>
Tästä seuraa ohjelmoijalle tärkeä päätelmä: jos otat role-attribuutin käyttöön, joudut toteuttamaan kaiken käyttäytymisen itse — fokusoituvuuden, näppäinkomennot ja tilan ylläpidon. Se on paljon työtä ja helppo tehdä puolittain.
Milloin ARIAa käytetään
ARIAn ensimmäinen sääntö — älä käytä ARIAa, jos tehtävään on olemassa HTML-elementti — on käyty läpi ITKP1011-kurssilla. Sama pätee tässä, mutta tällä kurssilla sivu muuttuu ajon aikana, joten ARIAa tarvitaan enemmän kuin yhteen asiaan.
Käytännön järjestys pysyy silti samana:
- oikea HTML-elementti
- jos elementtiä ei ole, ARIA täydentämään merkkausta
- omat roolit vasta kun molemmat edellä on käyty läpi
ARIA Authoring Practices Guide luettelee valmiit roolit, tilat ja näppäinkomennot tavallisimmille käyttöliittymän osille. Se on tämän aiheen tärkein yksittäinen lähde.
Saavutettavuuspuun katsominen
Saavutettavuuspuu näkyy kehittäjätyökaluissa: Firefoxissa välilehdellä Accessibility ja Chromessa Elements-välilehden Accessibility-paneelissa. Sieltä näkee jokaisen elementin roolin ja nimen sellaisena kuin apuväline sen saa.
Tämä on nopein tapa tarkistaa oma työ: jos elementin nimi on tyhjä tai rooli on generic, jotain on kesken.
Saavutettava nimi
Jokaisella vuorovaikutteisella elementillä on oltava saavutettava nimi (accessible name). Se on se teksti, jonka ruudunlukija lukee elementin kohdalla. Lomakekentän nimi tulee label-elementistä, mutta nimi voi tulla myös muualta.
Nimi lasketaan useasta lähteestä, ja järjestys on määritelty. Ensimmäinen löytyvä voittaa:
| Järjestys | Lähde |
|---|---|
| 1 | aria-labelledby — viittaus toisen elementin tunnisteeseen |
| 2 | aria-label — teksti attribuutissa |
| 3 | elementin oma tapa: label, alt, legend, caption |
| 4 | elementin sisältö, esimerkiksi painikkeen teksti |
| 5 | title-attribuutti — viimeinen oljenkorsi |
Järjestyksestä seuraa yleinen virhe. aria-label ohittaa elementin oman sisällön, joten seuraavassa ruudunlukija lukee vain sanan "Sulje" — näkyvä teksti katoaa kokonaan:
<!-- Näkyvä teksti ja luettu nimi eroavat. Puheohjausta käyttävä
sanoo "Peruuta" eikä mitään tapahdu. -->
<button aria-label="Sulje">Peruuta</button>
Sääntö: jos elementillä on näkyvä teksti, se kuuluu nimeen. aria-label on tarkoitettu elementeille, joilla ei ole tekstiä — tyypillisesti kuvakepainikkeille.
// Kuvakepainike: nimi on pakko antaa erikseen.
// Mukaan kuuluu myös se, MITÄ poistetaan - ruudunlukija lukee painikkeet
// myös irrallaan asiayhteydestään.
nappi.setAttribute("aria-label", `Poista ${auto.malli} ${auto.valmistaja}`);
aria-labelledby on usein parempi kuin aria-label, koska se viittaa sivulla jo olevaan tekstiin eikä kahdenna sitä. Huomaa, että viittaus etsitään samasta puusta — tunniste ei löydy shadow DOM:in toiselta puolelta.
Tila ja ominaisuudet
Kun javascript muuttaa elementin tilaa, tila on kerrottava myös ARIAlla. Nämä ovat tavallisimmat.
| Attribuutti | Mihin |
|---|---|
| aria-expanded | onko painikkeen avaama alue auki — valikot, haitarit |
| aria-pressed | onko painike päällä — tilaa vaihtava painike |
| aria-current | mikä on nykyinen sivu, vaihe tai rivi |
| aria-selected | valinta listassa tai välilehdissä |
| aria-describedby | lisäselitys, esimerkiksi virheilmoitus tai ohjeteksti |
| aria-invalid | onko kentän arvo kelvoton |
| aria-busy | alue on päivittymässä |
// Valikkopainike: tila kerrotaan aina kun se vaihtuu
const avaa = function () {
const auki = valikko.hidden;
valikko.hidden = !auki;
nappi.setAttribute("aria-expanded", String(auki));
};
Huomaa String(auki). ARIAn arvot ovat merkkijonoja, ja attribuutin puuttuminen ei tarkoita arvoa false vaan sitä, ettei tilaa ole kerrottu lainkaan. Totuusarvoiset ARIA-attribuutit kirjoitetaan siis näkyviin molemmissa tapauksissa — toisin kuin HTML:n disabled ja required.
aria-hidden ja disabled
aria-hidden="true" poistaa elementin saavutettavuuspuusta mutta jättää sen näkyviin ja fokusoitavaksi. Se on oikea valinta vain koristeille — esimerkiksi kuvakkeelle, jonka merkitys on jo painikkeen tekstissä.
<button type="button">
<span aria-hidden="true">×</span> Sulje
</button>
Älä koskaan aseta sitä fokusoitavalle elementille. Silloin syntyy tila, jossa näppäimistökäyttäjä saa fokuksen elementtiin josta ruudunlukija ei kerro mitään.
Samoin aria-disabled="true" kertoo elementin olevan poissa käytöstä mutta ei estä sen käyttöä — toisin kuin HTML:n disabled. Ero on siinä, että disabled-elementti poistuu myös näppäinkierrosta, joten käyttäjä ei löydä sitä eikä saa selitystä siitä miksi se ei toimi. Siksi aria-disabled on joskus parempi, mutta silloin toiminto on estettävä itse käsittelijässä.
Fokus
Fokus on se elementti, joka vastaanottaa näppäinpainallukset. Se on näppäimistö- ja ruudunlukijakäyttäjälle sama asia kuin kursorin sijainti hiirikäyttäjälle: jos se katoaa tai hyppää väärään paikkaan, käyttäjä on eksyksissä.
Järjestys tulee DOM-puusta
Näppäinkierron järjestys on DOM-puun järjestys, ei se, miten elementit näkyvät ruudulla. Tästä seuraa, että CSS:llä uudelleenjärjestetty sisältö — order-ominaisuus flexissä tai grid-area-sijoittelu — voi tuottaa fokusjärjestyksen, joka hyppii näytöllä sinne tänne.
Korjaus on järjestää DOM-puu oikein, ei säätää tabindex-arvoja.
| tabindex | Vaikutus |
|---|---|
| ei attribuuttia | vuorovaikutteiset elementit ovat kierrossa, muut eivät |
| 0 | elementti tulee kiertoon DOM-järjestyksessä |
| -1 | ei tule kiertoon, mutta voidaan fokusoida ohjelmallisesti |
| positiivinen luku | rikkoo järjestyksen koko sivulla — älä käytä |
tabindex="-1" on se, jota ohjelmoija oikeasti tarvitsee: se tekee elementistä fokusoitavan focus-metodilla ilman että se tulee näppäinkiertoon.
:focus-visible
Selain piirtää fokusoidun elementin ympärille reunuksen. Sitä ei saa poistaa — ilman sitä näppäimistökäyttäjä ei näe missä on.
Jos selaimen oma reunus ei sovi ulkoasuun, se korvataan omalla. Käytä :focus-visible-valitsinta: se osuu silloin kun selain arvioi, että reunus kannattaa näyttää — eli näppäimistöllä muttei hiirellä klikatessa.
/* EI näin: poistaa reunuksen kaikilta */
:focus { outline: none; }
/* Näin: oma reunus, joka näkyy kun sitä tarvitaan */
:focus-visible {
outline: 3px solid;
outline-offset: 2px;
}
Fokuksen siirtäminen ohjelmallisesti
Fokus on siirrettävä itse aina kun sivun rakenne muuttuu niin, että fokus katoaisi. Tavallisimmat tilanteet:
- Rivin poistaminen listasta. Jos fokus oli poistetun rivin poistopainikkeessa, se katoaa ja siirtyy dokumentin alkuun. Siirrä se seuraavaan riviin tai, jos lista tyhjeni, listan otsikkoon.
- Näkymän vaihtuminen. Yhden sivun sovelluksessa selain ei lataa mitään, joten fokus jää vanhaan paikkaan. Siirrä se uuden näkymän otsikkoon.
- Dialogin avaaminen ja sulkeminen. Kts. tapahtumaluennon luku dialogeista.
- Virheen jälkeen. Siirrä fokus ensimmäiseen virheelliseen kenttään tai virheyhteenvetoon. Kts. validointi.
const poista = function (rivi) {
// Otetaan talteen mihin fokus siirretään, ENNEN kuin rivi poistetaan
const seuraava = rivi.nextElementSibling ?? rivi.previousElementSibling;
rivi.remove();
if (seuraava !== null) {
seuraava.querySelector("button").focus();
}
else {
// lista tyhjeni: siirretään fokus otsikkoon, joka ei ole
// luontaisesti fokusoitava -> tabindex="-1"
otsikko.focus();
}
};
Otsikko tarvitsee tabindex="-1"-attribuutin, jotta focus-metodi toimii. Älä anna sille arvoa 0: se lisäisi otsikon näppäinkiertoon, mikä hämmentää.
focus-metodi vierittää elementin näkyviin. Jos sitä ei halua,
parametri { preventScroll: true } estää vierityksen.
Ilmoitukset ja aria-live-alueet
Kun javascript lisää sivulle tekstiä, ruudunlukija ei kerro siitä mitään. Näkevä käyttäjä huomaa uuden ilmoituksen heti, koska hän näkee koko sivun kerralla. Ruudunlukija lukee sivua kohta kohdalta, joten se ei palaa itsestään taaksepäin katsomaan mitä muualla tapahtui.
Ratkaisu on aria-live-alue: elementti, jonka sisällön muuttumisesta apuväline kertoo automaattisesti. Englanniksi termi on live region.
<p id="ilmoitus" aria-live="polite" class="vain-ruudunlukijalle"></p>
document.getElementById("ilmoitus").textContent = "Lomake tallennettiin";
| Arvo | Milloin |
|---|---|
| polite | lukee kun käyttäjä on tauolla — lähes aina oikea valinta |
| assertive | keskeyttää käynnissä olevan puheen — vain kiireelliselle |
Valmiit roolit tekevät saman lyhyemmin: role="status" vastaa polite-arvoa ja role="alert" assertive-arvoa.
Alueen on oltava sivulla ennen kuin sen sisältö muuttuu. Tämä on aiheen tavallisin virhe: jos alue luodaan javascriptilla ja täytetään heti, apuväline ei ehdi huomata sitä eikä ilmoitusta lueta. Lisää tyhjä alue merkkaukseen ja kirjoita siihen vasta myöhemmin.
Alue piilotetaan näkyvistä niin, että apuväline silti lukee sen — samalla tavalla kuin ITKP1011:n hyppylinkki, eli ei display: none -määrityksellä:
.vain-ruudunlukijalle {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
Älä käytä aria-live-aluetta kaikkeen. Jos ilmoitus on tärkeä, se kannattaa näyttää myös näkyvästi — ja silloin erillistä piilotettua aluetta ei välttämättä tarvita lainkaan.
Omat komponentit ja näppäimistö
Kun teet oman käyttöliittymän osan — välilehdet, haitarin, yhdistelmäkentän — joudut toteuttamaan näppäinkomennot itse. Älä keksi niitä: käyttäjät odottavat samoja komentoja kuin muualla.
ARIA Authoring Practices Guide luettelee jokaiselle tavalliselle osalle roolit, tilat ja näppäinkomennot.
Kolme periaatetta, jotka kattavat suurimman osan tapauksista:
- Klikattava asia on button. Silloin fokus, enter, välilyönti ja rooli tulevat valmiina. div-elementti onClick-käsittelijällä ei ole näppäimistöllä käytettävissä.
- Nuolinäppäimet kuuluvat ryhmän sisälle, sarkain ryhmien välille. Välilehdissä ja valikoissa koko ryhmä on yksi kohde näppäinkierrossa, ja sen sisällä liikutaan nuolilla. Tämä toteutetaan antamalla valitulle kohteelle tabindex="0" ja muille tabindex="-1".
- Esc peruu. Jos jotain avautuu, Esc sulkee sen.
Ajastukset ja automaattinen liike
prefers-reduced-motion-kysely on käsitelty animointiluennolla ja raahaamisen vaihtoehdot drag & drop -luennolla. Tähän jää kaksi asiaa, jotka ovat ohjelman ajastuksista kiinni.
- Itsestään käynnistyvä liike tarvitsee pysäytyspainikkeen, jos se kestää yli viisi sekuntia. Karuselli, joka vaihtaa kuvaa itsestään, on tavallisin tapaus.
- Itsestään katoava sisältö tarvitsee lisäaikaa. Kolmen sekunnin ilmoitus ehtii kadota ennen kuin ruudunlukija on lukenut sen loppuun. Jätä ilmoitus näkyviin, kunnes käyttäjä sulkee sen, tai anna tapa pidentää aikaa.
Muuttuvan sivun testaaminen
ITKP1011:n testauslista — validointi, sarkaimella läpi, tyylit pois, kuvat pois, tekstin suurentaminen, kontrastit ja ruudunlukijan kokeileminen — pätee sellaisenaan. Se tarkastaa sivun yhdessä tilassa.
Javascript-sovelluksessa tiloja on monta, eikä listan läpikäyminen kerran riitä. Kolme lisätarkistusta kohdistuu nimenomaan muutoksiin:
- Tee jokainen toiminto ja katso, mihin fokus menee. Avaa dialogi ja sulje se. Poista rivi. Lähetä lomake virheellisenä. Vaihda näkymää. Joka kerta sama kysymys: onko fokus jossain järkevässä, vai hyppäsikö se dokumentin alkuun?
- Tarkista, kertooko jokainen muutos itsestään. Kun jotain ilmestyy, katoaa tai valmistuu, tuleeko siitä ilmoitus — vai näkyykö muutos vain silmällä?
- Katso saavutettavuuspuu muutoksen jälkeen. Onko jokaisella painikkeella nimi? Kertooko aria-expanded totuuden avaamisen jälkeen? Jäikö aria-hidden päälle?
Kolmas kohta on se, jonka automaattiset työkalut jättävät kokonaan huomaamatta: ne tarkastavat sivun sellaisena kuin se on latautuessaan, eivät sellaisena kuin se on viiden klikkauksen jälkeen.
Lomakkeiden ulkoasu
Kertaa Asemointi CSS:n avulla.
Päivityslomakkeet
Seuraavassa on neljä eritasoista esimerkkiä, joissa päivitetään yksinkertaista tietorakennetta ja esitetään sivulla tietorakenteen sisältö.
Versio 0
Tässä esimerkissä ongelmana on muutosten päivittäminen suoraan tietorakenteeseen. Mitä jos käyttäjä kirjoittaa jotain väärin? Muutosta ei voi perua, koska se on heti peruuttamaton. Parempi ratkaisu olisi muokata kopiota ja päivittää tietorakenne vasta kun klikataan tallenna-painiketta. Tästä näkee kuitenkin, miten objektien viitteitä voi käyttää: viitteiden avulla tietorakenteen ja DOM-puun käsittely on pidetty minimissä.
muokkaus 0 · muokkaus 0 + live-editori
Käynnistä sovellus run-painikkeella:
Versio 1
Sama kuin edellä, mutta muokkaaminen tehdään kopion kautta. Tietorakenne päivitetään vasta kun on painettu tallenna-painiketta.
Käynnistä sovellus run-painikkeella:
Versio 2
Sama kuin edellä, mutta tietorakennetta on monimutkaistettu lisäämällä siihen yksinkertainen taulukko. Tämä vastaa viikkotehtävän tilannetta, jossa joukkueeseen lisätään jäseniä. Tässä käytetään uudelleen samoja input-kenttiä eikä siis luoda lomaketta joka kerta uudelleen.
muokkaus 2 · muokkaus 2 + live-editori
Käynnistä sovellus run-painikkeella:
Versio 3
Tämä on täysin dynaaminen versio: koodiin ei ole kovakoodattu mitään tietorakenteen avaimia. Rakenteeseen voi suoraan lisätä uusia taulukoita tai merkkijonotyyppisiä tietoja. Muita tietotyyppejä ei ole huomioitu. Tässä ei myöskään pärjätä pelkän viitteen avulla, vaan päivitys tehdään taulukon indeksin perusteella — se on yllättäen tässä tilanteessa yksinkertaisin vaihtoehto. Viitettä käyttäessä täytyisi erikseen päivittää kaikki objektin ominaisuudet, mutta indeksin avulla taulukossa oleva alkio voidaan suoraan vaihtaa viittaamaan muokkauksessa käytettyyn kopioon.
Tämä ei ole millään tavalla parempi versio kuin edellinen, ellei ihan oikeasti ole tarvetta dynaamiseen laajenemiseen. Edellinen versio on yksinkertaisempi ja aivan riittävä, jos rakenne on tiedossa.
Harjoitustehtävänä voi miettiä, miten näitä muuttaisi, jos haluttaisiin mahdollistaa uusien tietojen lisääminen. Nämä osaavat nyt vasta muokata.
Lomake-esimerkkejä
- Lomakkeiden ohjelmallinen käsittely — katso esimerkin lähdekoodi
- Click- ja submit-tapahtumat (lomake.js)
- Lomakkeen asemointi CSS:llä (css.css)
- Dynaaminen lomake (dynaaminen.js), joka lisää ja poistaa syöttökenttiä tarpeen mukaan
Lisätietoa
- ITKP1011: Saavutettavuus ja käytettävyys — perusteet, WCAG ja testauslista
- ARIA Authoring Practices Guide
- Using ARIA
- How to Meet WCAG 2.2
- Web forms — MDN
- Client-side form validation
- Styling web forms
- ValidityState
- FormData
- :user-invalid ja :user-valid
- DOM ja tapahtumat — tapahtumankäsittelyn perusteet
Käyttäjien kommentit