Webbutveckling 1 · område 6 · bibliotek
Modulbiblioteket
Sex moduler. Alla sex är inbyggda i områdets exempelsajt, och varje modul har en egen demosida med den minsta HTML den behöver.
Du väljer minst två till Projekt 5. Hur du väljer står i uppgiften – men välj efter vad din sajt behöver, inte efter vilken som ser enklast ut.
Så använder du sidan
Varje modul har fem delar:
| Del | Vad du gör med den |
|---|---|
| Vad den gör | Läs först. Passar modulen din sajt? |
| HTML:en du behöver | Kopiera in i din sida, på rätt plats |
| Koden | Kopiera hela filen till js/ i ditt repo |
| Ändra här | De rader du får byta. Allt annat lämnar du |
| Skriv den själv | A-spåret: kraven, utan kod |
Kodblocken på den här sidan går att markera och kopiera – till skillnad från guidens, som är till för att skrivas av. Det är med avsikt: modulerna ska du använda, inte träna på att skriva.
Glöm inte CSS:en. Varje modul har ett avsnitt i referensprojekt/stil.css, märkt 13a till 13f, och det ska med.
Fem steg för inkopplingen står i JS-guiden avsnitt 8. Fastnar du: avsnitt 9, konsolen, innan du ändrar något.
1. Hamburgermeny
Fil: js/meny.js · CSS: avsnitt 13a · Demo: demo/meny.html · På exempelsajten: alla sidor
Vad den gör
På smala skärmar göms menyn bakom en knapp, och knappen öppnar och stänger den. På breda skärmar syns menyn som vanligt och knappen är borta – det sköter CSS:en, inte skriptet. Escape stänger menyn och flyttar fokus tillbaka till knappen.
Utan JavaScript: knappen har hidden i HTML:en och menyn syns som vanligt. Sidan fungerar, den har bara ingen hamburgermeny.
Passar när din meny har fler än tre–fyra länkar och tar för mycket plats på mobil.
HTML:en du behöver
<nav class="huvudmeny" aria-label="Huvudmeny">
<button type="button" class="menyknapp" hidden
aria-expanded="false" aria-controls="huvudmeny-lista">Meny</button>
<ul id="huvudmeny-lista">
<li><a href="index.html">Start</a></li>
<li><a href="om.html">Om oss</a></li>
</ul>
</nav>
hidden på knappen är inte ett misstag – det är det som gör att sidan fungerar utan skriptet. Ta inte bort det.
Koden
/* ==========================================================================
Hamburgermeny – modul 1 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: på smala skärmar göms menyn bakom en knapp. Knappen öppnar
och stänger den. På breda skärmar syns menyn som vanligt och knappen är
borta – det sköter CSS:en, inte skriptet.
Utan JavaScript: knappen har attributet hidden i HTML:en och menyn syns
som vanligt. Sidan fungerar, den har bara ingen hamburgermeny.
Kräver i HTML:
<nav class="huvudmeny">
<button type="button" class="menyknapp" hidden
aria-expanded="false"
aria-controls="huvudmeny-lista">Meny</button>
<ul id="huvudmeny-lista"> … </ul>
</nav>
Kräver i CSS: avsnittet "13a. Hamburgermenyn" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ======================================================
Det här är de rader du kan behöva byta när du flyttar modulen till din
egen sajt. Klassnamn ska stämma med din HTML, texterna får vara vad du
vill. Ingenting annat i filen behöver röras.
====================================================================== */
const KNAPP = ".menyknapp"; // knappen som öppnar och stänger
const LISTA = "#huvudmeny-lista"; // listan som göms och visas
const TEXT_STANGD = "Meny"; // knappens text när menyn är stängd
const TEXT_OPPEN = "Stäng"; // knappens text när menyn är öppen
/* ===== SLUT PÅ ÄNDRA HÄR – nedanför behöver ingenting ändras ========= */
// Leta upp knappen och listan. Finns de inte på sidan avslutar vi tyst –
// ett skript ska aldrig krascha för att sidan saknar det det väntar sig.
const knapp = document.querySelector(KNAPP);
const lista = document.querySelector(LISTA);
if (!knapp || !lista) { return; }
// Skriptet fungerar, så nu får knappen synas. Utan skript stannar hidden
// kvar och menyn visas som vanligt – det är det som gör modulen diskret.
knapp.hidden = false;
knapp.textContent = TEXT_STANGD;
// aria-expanded säger både för skärmläsaren och för CSS:en om menyn är
// öppen. CSS:en visar listan när värdet är "true" – skriptet ändrar bara
// attributet och låter stilmallen göra resten.
function oppna() {
knapp.setAttribute("aria-expanded", "true");
knapp.textContent = TEXT_OPPEN;
}
function stang() {
knapp.setAttribute("aria-expanded", "false");
knapp.textContent = TEXT_STANGD;
}
// Ett klick på knappen växlar: öppen blir stängd, stängd blir öppen.
knapp.addEventListener("click", function () {
const arOppen = knapp.getAttribute("aria-expanded") === "true";
if (arOppen) {
stang();
} else {
oppna();
}
});
// Escape stänger menyn och lägger fokus tillbaka på knappen, så att den
// som navigerar med tangentbord vet var hen är.
document.addEventListener("keydown", function (handelse) {
const arOppen = knapp.getAttribute("aria-expanded") === "true";
if (handelse.key === "Escape" && arOppen) {
stang();
knapp.focus();
}
});
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
KNAPP | Klassen på din knapp, med punkt först |
LISTA | Id:t på din lista, med brädgård först |
TEXT_STANGD | Vad knappen ska säga när menyn är stängd |
TEXT_OPPEN | Vad den ska säga när menyn är öppen |
Byter du LISTA måste du byta id i HTML:en och aria-controls på knappen.
Skriv den själv
Kraven, utan kod. Skriv modulen från grunden och jämför sedan.
- Hitta knappen och listan. Avsluta tyst om någon av dem saknas.
- Ta bort
hiddenfrån knappen när skriptet körs, och sätt dess text. - Ett klick växlar
aria-expandedmellan"true"och"false"och byter knappens text därefter. Skriptet ska inte gömma eller visa listan självt – CSS:en gör det utifrån attributet. - Escape stänger menyn om den är öppen, och flyttar fokus till knappen.
2. Mörkt läge med sparat val
Fil: js/morkt-lage.js · CSS: avsnitt 13b · Demo: demo/morkt-lage.html · På exempelsajten: alla sidor
Vad den gör
En knapp växlar mellan ljust och mörkt läge, och valet sparas i webbläsaren så att det gäller nästa gång också. Har besökaren inte valt följer sidan systemets inställning, precis som förut.
Skriptet sätter data-lage="mork" eller data-lage="ljus" på html. Stilmallen har ett färgschema för vardera värdet. Skriptet vet ingenting om färger – det byter ett attribut och låter stilmallen göra resten.
Utan JavaScript: knappen är gömd, och @media (prefers-color-scheme) i stilmallen styr som vanligt.
Passar när din sajt redan har ett mörkt schema via prefers-color-scheme från område 3. Har den inte det är det den delen du ska bygga först.
HTML:en du behöver
<button type="button" class="lagesknapp" hidden aria-pressed="false">Mörkt läge</button>
Lägg den i header, eller där den syns på varje sida.
Koden
/* ==========================================================================
Mörkt läge med sparat val – modul 2 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: en knapp växlar mellan ljust och mörkt läge, och valet sparas
i webbläsaren så att det gäller nästa gång också. Har besökaren inte valt
följer sidan systemets inställning, precis som förut.
Så hänger det ihop med CSS:en: skriptet sätter data-lage="mork" eller
data-lage="ljus" på <html>. Stilmallen har ett färgschema för vardera
värdet. Skriptet vet ingenting om färger – det byter ett attribut och
låter stilmallen göra resten.
Utan JavaScript: knappen är hidden, och @media (prefers-color-scheme)
i stilmallen styr som vanligt.
Kräver i HTML:
<button type="button" class="lagesknapp" hidden
aria-pressed="false">Mörkt läge</button>
Kräver i CSS: avsnittet "13b. Mörkt läge" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ====================================================== */
const KNAPP = ".lagesknapp"; // knappen som växlar
// Namnet valet sparas under. Byt till din sajts namn:
const NYCKEL = "vikstad-lage";
const TEXT_TILL_MORK = "Mörkt läge"; // knappens text när sidan är ljus
const TEXT_TILL_LJUS = "Ljust läge"; // knappens text när sidan är mörk
/* ===== SLUT PÅ ÄNDRA HÄR ============================================== */
const knapp = document.querySelector(KNAPP);
if (!knapp) { return; }
const rot = document.documentElement; // <html>
// localStorage kastar ett fel i vissa lägen – privat fönster, en fil
// öppnad från en mapp, en webbläsare som blockerar lagring. try/catch gör
// att sidan fungerar ändå; valet sparas bara inte.
function lasSparat() {
try {
return localStorage.getItem(NYCKEL);
} catch (fel) {
return null;
}
}
function spara(lage) {
try {
localStorage.setItem(NYCKEL, lage);
} catch (fel) {
// Går inte att spara. Läget gäller ändå tills sidan laddas om.
}
}
// Vilket läge har sidan just nu? Har besökaren valt gäller valet. Annars
// frågar vi systemet: matchMedia svarar på samma fråga som CSS:ens
// @media (prefers-color-scheme: dark).
function aktuelltLage() {
const sparat = lasSparat();
if (sparat === "mork" || sparat === "ljus") { return sparat; }
const morkt = window.matchMedia("(prefers-color-scheme: dark)").matches;
return morkt ? "mork" : "ljus";
}
// Sätt attributet på <html> och uppdatera knappen så att den säger vad ett
// klick kommer att göra – inte vad läget är just nu.
function visa(lage) {
rot.dataset.lage = lage;
const arMork = lage === "mork";
knapp.textContent = arMork ? TEXT_TILL_LJUS : TEXT_TILL_MORK;
knapp.setAttribute("aria-pressed", arMork ? "true" : "false");
}
// Starta: visa rätt läge direkt, och först därefter visa knappen.
visa(aktuelltLage());
knapp.hidden = false;
knapp.addEventListener("click", function () {
const nytt = rot.dataset.lage === "mork" ? "ljus" : "mork";
visa(nytt);
spara(nytt);
});
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
KNAPP | Klassen på din knapp |
NYCKEL | Namnet valet sparas under. Byt till din sajts namn – annars delar två sajter på samma dator samma val |
TEXT_TILL_MORK | Knappens text när sidan är ljus |
TEXT_TILL_LJUS | Knappens text när sidan är mörk |
Varför try och catch?
localStorage kastar ett fel i vissa lägen: privat fönster, en fil öppnad direkt från en mapp, en webbläsare som blockerar lagring. Utan try/catch hade hela modulen kraschat där. Med den fungerar läget ändå – valet sparas bara inte.
Skriv den själv
- Hitta knappen. Avsluta tyst om den saknas.
- Avgör vilket läge som gäller: besökarens sparade val om det finns, annars systemets (
window.matchMedia("(prefers-color-scheme: dark)").matches). - Sätt
data-lagepådocument.documentElement, uppdatera knappens text ocharia-pressed, och visa knappen. - Ett klick byter läge, visar det nya och sparar det.
- All läsning och skrivning till
localStorageska ligga itry/catch.
3. Filtrerbart galleri
Fil: js/galleri.js · CSS: avsnitt 13c · Demo: demo/galleri.html · På exempelsajten: djur-och-vaxter.html
Vad den gör
En rad knappar filtrerar innehållet. Varje del som ska kunna filtreras har ett data-grupp-attribut, och ett klick på en knapp visar bara de delar som hör till den gruppen. "Alla" visar allt igen.
Knapparna byggs av skriptet, ur de grupper som faktiskt finns i HTML:en. Lägger du till en ny grupp kommer en ny knapp av sig själv – men den behöver en text i GRUPPNAMN.
Utan JavaScript: allt innehåll visas, och ingen filterrad finns. Det är rätt: utan skript ska besökaren se allt, inte ingenting.
Passar när du har en samling av samma sorts innehåll i tydliga kategorier: bilder, recept, projekt, arter.
HTML:en du behöver
<div class="filter" role="group" aria-label="Visa" hidden></div>
<section data-grupp="faglar">
<h2>Fåglar</h2>
<p>…</p>
</section>
<section data-grupp="daggdjur">
<h2>Däggdjur</h2>
<p>…</p>
</section>
Filterraden är tom – skriptet fyller den. data-grupp sitter på det som ska filtreras, inte på raden.
Koden
/* ==========================================================================
Filtrerbart galleri – modul 3 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: en rad knappar filtrerar innehållet. Varje del av innehållet
har ett data-grupp-attribut, och ett klick på en knapp visar bara de delar
som hör till den gruppen. "Alla" visar allt igen.
Knapparna byggs av skriptet, ur de grupper som faktiskt finns i HTML:en.
Lägger du till en ny grupp i HTML:en kommer en ny knapp av sig själv –
men den behöver en text, se ÄNDRA HÄR.
Utan JavaScript: allt innehåll visas, och ingen filterrad finns. Det är
rätt: utan skript ska besökaren se allt, inte ingenting.
Kräver i HTML:
<div class="filter" role="group" aria-label="Visa" hidden></div>
… ← skriptet fyller den
…
<section data-grupp="faglar"> … </section>
<section data-grupp="daggdjur"> … </section>
Kräver i CSS: avsnittet "13c. Filtret" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ====================================================== */
const FILTERRAD = ".filter"; // behållaren knapparna hamnar i
const INNEHALL = "[data-grupp]"; // allt som ska kunna filtreras
const TEXT_ALLA = "Alla"; // texten på knappen som visar allt
// Texten på varje gruppknapp. Nyckeln är värdet i data-grupp i HTML:en.
// Saknas en grupp här får knappen gruppens namn som det står i HTML:en.
const GRUPPNAMN = {
faglar: "Fåglar",
daggdjur: "Däggdjur",
vaxter: "Växter"
};
/* ===== SLUT PÅ ÄNDRA HÄR ============================================== */
const filterrad = document.querySelector(FILTERRAD);
const delar = document.querySelectorAll(INNEHALL);
if (!filterrad || delar.length === 0) { return; }
// Vilka grupper finns? Vi går igenom innehållet och samlar varje värde
// en gång. Ordningen blir den ordning grupperna dyker upp i på sidan.
const grupper = [];
delar.forEach(function (del) {
const grupp = del.dataset.grupp;
if (grupper.indexOf(grupp) === -1) {
grupper.push(grupp);
}
});
// En knapp per grupp, plus "Alla" först. aria-pressed säger vilken som är
// vald – för skärmläsaren och för CSS:en, som färgar den valda knappen.
function skapaKnapp(varde, text) {
const knapp = document.createElement("button");
knapp.type = "button";
knapp.textContent = text;
knapp.dataset.valj = varde;
knapp.setAttribute("aria-pressed", varde === "alla" ? "true" : "false");
filterrad.appendChild(knapp);
return knapp;
}
skapaKnapp("alla", TEXT_ALLA);
grupper.forEach(function (grupp) {
skapaKnapp(grupp, GRUPPNAMN[grupp] || grupp);
});
// Själva filtreringen: göm allt som inte hör till gruppen. hidden är ett
// vanligt HTML-attribut, så CSS:en behöver inte känna till skriptet.
function filtrera(grupp) {
delar.forEach(function (del) {
del.hidden = grupp !== "alla" && del.dataset.grupp !== grupp;
});
filterrad.querySelectorAll("button").forEach(function (knapp) {
const vald = knapp.dataset.valj === grupp;
knapp.setAttribute("aria-pressed", vald ? "true" : "false");
});
}
// Ett lyssnare på hela raden räcker. Klicket "bubblar" upp från knappen,
// och vi läser av vilken knapp det var. Det kallas händelsedelegering.
filterrad.addEventListener("click", function (handelse) {
const knapp = handelse.target.closest("button");
if (!knapp) { return; }
filtrera(knapp.dataset.valj);
});
// Knapparna finns, så nu får raden synas.
filterrad.hidden = false;
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
FILTERRAD | Klassen på din tomma div |
INNEHALL | Selektorn för det som ska filtreras. [data-grupp] fungerar oftast som den är |
TEXT_ALLA | Texten på knappen som visar allt |
GRUPPNAMN | En rad per grupp: nyckel: "Text på knappen". Nyckeln är värdet i data-grupp |
Saknas en grupp i GRUPPNAMN får knappen gruppens namn rakt av, som det står i HTML:en. Det fungerar, men blir sällan snyggt.
Skriv den själv
- Hitta filterraden och allt med
data-grupp. Avsluta tyst om något saknas. - Samla ihop vilka grupper som finns, en gång per unikt värde, i den ordning de dyker upp.
- Skapa en knapp per grupp plus en "Alla" först. Den valda knappen ska ha
aria-pressed="true", de andra"false". - Ett klick gömmer allt som inte hör till gruppen, med
hidden, och flyttararia-pressed. - Använd en lyssnare på raden, inte en per knapp.
4. Bildkarusell
Fil: js/karusell.js · CSS: avsnitt 13d · Demo: demo/karusell.html · På exempelsajten: stigar.html
Vad den gör
Visar en bild i taget ur en lista, med knappar för föregående och nästa, piltangenter, och en rad som säger "Bild 2 av 3".
Den bläddrar aldrig av sig själv. En karusell som rör sig utan att någon bett om det är ett tillgänglighetsproblem, inte en funktion: den som läser långsamt hinner inte, och den som slagit på "minska rörelse" har bett om att slippa det.
Utan JavaScript: alla bilder visas efter varandra, som en vanlig lista med figurer. Ingenting saknas, det tar bara mer plats.
Passar när du har tre till sju bilder som hör ihop och tar för mycket höjd var för sig. Har du två räcker det att visa båda.
HTML:en du behöver
<div class="karusell" role="group" aria-label="Bilder">
<ul class="karusell-bilder">
<li>
<figure>
<img src="bilder/ett.jpg" width="480" height="300" alt="Beskriv bilden">
<figcaption>Bildtext.</figcaption>
</figure>
</li>
<li>
<figure>
<img src="bilder/tva.jpg" width="480" height="300" alt="Beskriv bilden">
<figcaption>Bildtext.</figcaption>
</figure>
</li>
</ul>
<div class="karusell-styr" hidden>
<button type="button" class="karusell-forra">Föregående</button>
<p class="karusell-lage" aria-live="polite"></p>
<button type="button" class="karusell-nasta">Nästa</button>
</div>
</div>
aria-live="polite" på lägesraden gör att en skärmläsare läser upp "Bild 2 av 3" när texten byts, utan att avbryta det som just lästes.
Koden
/* ==========================================================================
Bildkarusell – modul 4 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: visar en bild i taget ur en lista, med knappar för föregående
och nästa, piltangenter, och en rad som säger "Bild 2 av 3". Den bläddrar
aldrig av sig själv – en karusell som rör sig utan att någon bett om det
är ett tillgänglighetsproblem, inte en funktion.
Utan JavaScript: alla bilder visas efter varandra, som en vanlig lista med
figurer. Ingenting saknas, det tar bara mer plats.
Kräver i HTML:
<div class="karusell" role="group" aria-label="Bilder">
<ul class="karusell-bilder">
<li><figure> … </figure></li>
<li><figure> … </figure></li>
</ul>
<div class="karusell-styr" hidden>
<button type="button" class="karusell-forra">Föregående</button>
<p class="karusell-lage" aria-live="polite"></p>
<button type="button" class="karusell-nasta">Nästa</button>
</div>
</div>
Kräver i CSS: avsnittet "13d. Karusellen" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ====================================================== */
const KARUSELL = ".karusell";
const BILDER = ".karusell-bilder > li";
const STYR = ".karusell-styr";
const FORRA = ".karusell-forra";
const NASTA = ".karusell-nasta";
const LAGE = ".karusell-lage";
// {nr} och {antal} byts ut av skriptet:
const LAGETEXT = "Bild {nr} av {antal}";
// true: efter sista bilden kommer första igen. false: stanna vid kanten.
const BORJA_OM = true;
/* ===== SLUT PÅ ÄNDRA HÄR ============================================== */
const karusell = document.querySelector(KARUSELL);
if (!karusell) { return; }
const bilder = karusell.querySelectorAll(BILDER);
const styr = karusell.querySelector(STYR);
const forra = karusell.querySelector(FORRA);
const nasta = karusell.querySelector(NASTA);
const lage = karusell.querySelector(LAGE);
if (bilder.length < 2 || !styr || !forra || !nasta || !lage) { return; }
let aktuell = 0; // index på bilden som visas. Första bilden är 0
// Visa en bild och göm de andra. Lägesraden uppdateras – och eftersom den
// har aria-live läser skärmläsaren upp den nya texten av sig själv.
function visa(index) {
aktuell = index;
bilder.forEach(function (bild, i) {
bild.hidden = i !== aktuell;
});
lage.textContent = LAGETEXT
.replace("{nr}", aktuell + 1)
.replace("{antal}", bilder.length);
// Utan BORJA_OM ska knapparna sluta fungera vid kanterna. disabled tar
// bort dem ur tabbordningen också, vilket är vad man vill.
if (!BORJA_OM) {
forra.disabled = aktuell === 0;
nasta.disabled = aktuell === bilder.length - 1;
}
}
function bakat() {
if (aktuell > 0) {
visa(aktuell - 1);
} else if (BORJA_OM) {
visa(bilder.length - 1);
}
}
function framat() {
if (aktuell < bilder.length - 1) {
visa(aktuell + 1);
} else if (BORJA_OM) {
visa(0);
}
}
forra.addEventListener("click", bakat);
nasta.addEventListener("click", framat);
// Piltangenterna fungerar när karusellen eller något i den har fokus.
// tabindex gör att själva karusellen går att tabba till.
karusell.tabIndex = 0;
karusell.addEventListener("keydown", function (handelse) {
if (handelse.key === "ArrowLeft") { bakat(); }
if (handelse.key === "ArrowRight") { framat(); }
});
// Klassen säger åt CSS:en att karusellen är igång – bilderna ska ligga i
// en ruta i stället för under varandra. Sedan visar vi första bilden och
// knapparna.
karusell.classList.add("karusell-igang");
visa(0);
styr.hidden = false;
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
KARUSELL, BILDER, STYR, FORRA, NASTA, LAGE | Dina klassnamn, om du inte behåller modulens |
LAGETEXT | Texten i lägesraden. {nr} och {antal} byts ut av skriptet |
BORJA_OM | true: efter sista bilden kommer första igen. false: knapparna slutar fungera vid kanterna |
BORJA_OM = false är rimligare när ordningen betyder något – en instruktion i steg, till exempel.
Skriv den själv
- Hitta karusellen och dess delar. Avsluta tyst om det är färre än två bilder.
- Visa en bild i taget med
hiddenoch håll reda på vilken med enlet. - Uppdatera lägesraden. Kom ihåg att index börjar på 0 men människor räknar från 1.
- Knapparna och piltangenterna bläddrar. Bestäm själv vad som händer vid kanterna, och gör det till en inställning.
- Visa styrningen och sätt en klass på karusellen först när skriptet körts – det är den klassen CSS:en använder för att lägga bilderna i en ruta.
5. Formulärvalidering
Fil: js/validering.js · CSS: avsnitt 13e · Demo: demo/validering.html · På exempelsajten: besok.html
Vad den gör
När formuläret skickas kontrolleras fälten, och varje fel får ett meddelande i klartext under fältet. Fältet märks med aria-invalid och kopplas till meddelandet med aria-describedby, så att en skärmläsare läser upp felet när fältet får fokus. Första felaktiga fältet får fokus.
Modulen ersätter inte HTML:s egen validering – den bygger på den. Vad som är fel avgörs av required, type="email", min och max i din HTML, via fältets checkValidity(). Skriptet gör bara meddelandena bättre och lägger dem där de syns.
Utan JavaScript: webbläsarens inbyggda validering gäller, med dess egna meddelanden. Formuläret fungerar precis som förut.
Passar när du har ett formulär med mer än två–tre fält. Den kräver ingen extra markup alls – bara att formuläret är rätt byggt från område 2.
HTML:en du behöver
Ingen ny. Modulen kräver bara att formuläret är som det ska vara:
<form>
<p>
<label for="namn">Namn</label>
<input type="text" id="namn" name="namn" required>
</p>
<p>
<label for="epost">E-postadress</label>
<input type="email" id="epost" name="epost" required>
</p>
<p>
<button type="submit">Skicka</button>
</p>
</form>
Varje fält behöver ett id och en label med for som pekar på det. Det är den etiketten modulen använder när den skriver "Fyll i namn."
Koden
/* ==========================================================================
Formulärvalidering med tillgängliga felmeddelanden – modul 5 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: när formuläret skickas kontrolleras fälten, och varje fel
får ett meddelande i klartext under fältet. Fältet märks med aria-invalid
och kopplas till meddelandet med aria-describedby, så att en skärmläsare
läser upp felet när fältet får fokus. Första felaktiga fältet får fokus.
Modulen ersätter inte HTML:s egen validering – den bygger på den. Vad som
är fel avgörs av required, type="email", min och max i HTML:en, via
fältets checkValidity(). Skriptet gör bara meddelandena bättre och lägger
dem där de syns.
Utan JavaScript: webbläsarens inbyggda validering gäller, med dess egna
meddelanden. Formuläret fungerar precis som förut.
Kräver i HTML: ett vanligt formulär med label kopplad till varje fält, och
required / type / min / max där det behövs. Ingenting mer.
Kräver i CSS: avsnittet "13e. Felmeddelanden" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ====================================================== */
// Vilket formulär. Har sidan flera: ge ditt ett id och skriv "#idet".
const FORMULAR = "form";
// Meddelandena. {namn} byts ut mot fältets etikett (texten i dess label).
const TEXT_TOMT = "Fyll i {namn}.";
const TEXT_EPOST = "Skriv en e-postadress med @ och punkt, "
+ "till exempel namn@exempel.se.";
const TEXT_FOR_LITET = "{namn} måste vara minst {min}.";
const TEXT_FOR_STORT = "{namn} får vara högst {max}.";
const TEXT_OVRIGT = "Kontrollera {namn}.";
/* ===== SLUT PÅ ÄNDRA HÄR ============================================== */
const formular = document.querySelector(FORMULAR);
if (!formular) { return; }
// novalidate stänger av webbläsarens egna felrutor – men bara när skriptet
// körs. Utan skript finns attributet inte, och webbläsaren validerar själv.
formular.setAttribute("novalidate", "");
// Fältets namn i klartext: texten i dess label, med gemener och utan
// parentesen "(frivilligt)" och liknande.
function namnPa(falt) {
const etikett = formular.querySelector('label[for="' + falt.id + '"]');
if (!etikett) { return "fältet"; }
return etikett.textContent.replace(/\(.*?\)/, "").trim().toLowerCase();
}
// Vilket meddelande? validity är webbläsarens egen bedömning – vi läser av
// den och väljer text. Ordningen spelar roll: ett tomt fält är tomt först.
function meddelande(falt) {
const v = falt.validity;
const namn = namnPa(falt);
let text;
if (v.valueMissing) { text = TEXT_TOMT.replace("{namn}", namn); }
else if (v.typeMismatch && falt.type === "email") { text = TEXT_EPOST; }
else if (v.rangeUnderflow) {
text = TEXT_FOR_LITET.replace("{namn}", namn).replace("{min}", falt.min);
}
else if (v.rangeOverflow) {
text = TEXT_FOR_STORT.replace("{namn}", namn).replace("{max}", falt.max);
}
else { text = TEXT_OVRIGT.replace("{namn}", namn); }
// Stor bokstav först, även när meddelandet börjar med fältets namn.
return text.charAt(0).toUpperCase() + text.slice(1);
}
// Varje fält får ett eget felelement, skapat första gången det behövs och
// återanvänt sedan. id:t behövs för aria-describedby.
function felElement(falt) {
const id = "fel-" + falt.id;
let fel = document.getElementById(id);
if (!fel) {
fel = document.createElement("p");
fel.id = id;
fel.className = "fel";
falt.insertAdjacentElement("afterend", fel);
}
return fel;
}
function visaFel(falt) {
const fel = felElement(falt);
fel.textContent = meddelande(falt);
falt.setAttribute("aria-invalid", "true");
falt.setAttribute("aria-describedby", fel.id);
}
function taBortFel(falt) {
const fel = document.getElementById("fel-" + falt.id);
if (fel) { fel.textContent = ""; }
falt.removeAttribute("aria-invalid");
falt.removeAttribute("aria-describedby");
}
// Kontrollera alla fält. Returnerar första felaktiga fältet, eller null.
function kontrollera() {
let forsta = null;
const falten = formular.querySelectorAll("input, select, textarea");
falten.forEach(function (falt) {
if (falt.checkValidity()) {
taBortFel(falt);
} else {
visaFel(falt);
if (!forsta) { forsta = falt; }
}
});
return forsta;
}
// Vid sändning: finns fel stoppar vi sändningen och flyttar fokus till det
// första felet. preventDefault är det som hindrar formuläret att skickas.
formular.addEventListener("submit", function (handelse) {
const forstaFel = kontrollera();
if (forstaFel) {
handelse.preventDefault();
forstaFel.focus();
}
});
// När besökaren rättar ett fält ska felet försvinna så fort det är rätt –
// men inte dyka upp medan hen fortfarande skriver. Därför lyssnar vi på
// input bara på fält som redan har ett fel.
formular.addEventListener("input", function (handelse) {
const falt = handelse.target;
if (falt.getAttribute("aria-invalid") === "true" && falt.checkValidity()) {
taBortFel(falt);
}
});
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
FORMULAR | "form" räcker om sidan har ett formulär. Har den flera: ge ditt ett id och skriv "#idet" |
TEXT_TOMT | Meddelandet för ett tomt fält. {namn} byts mot fältets etikett |
TEXT_EPOST | Meddelandet för en felaktig e-postadress |
TEXT_FOR_LITET, TEXT_FOR_STORT | För tal utanför min och max. {min} och {max} byts ut |
TEXT_OVRIGT | När inget av de andra passar |
Meddelandet får automatiskt stor bokstav, även när det börjar med fältets namn.
Skriv den själv
- Hitta formuläret. Sätt
novalidatepå det – men bara från skriptet, aldrig i HTML:en. - Vid sändning: gå igenom alla fält och använd
checkValidity(). Avgör vad som är fel med fältetsvalidity-objekt. - Skriv felet i ett eget element efter fältet. Elementet ska ha ett
id, och fältet ska peka på det medaria-describedbyoch märkas medaria-invalid="true". - Finns fel: stoppa sändningen och flytta fokus till det första.
- Ta bort felet när besökaren rättat fältet – men först när det faktiskt är rätt, inte medan hen skriver.
6. Quiz med rättning
Fil: js/quiz.js · CSS: avsnitt 13f · Demo: demo/quiz.html · På exempelsajten: quiz.html
Vad den gör
Frågorna står i HTML:en som ett vanligt formulär med alternativknappar. När besökaren trycker "Rätta" jämför skriptet svaren med facit, märker varje fråga som rätt eller fel, och skriver ut resultatet. Är någon fråga obesvarad säger den vilken och stannar där.
Facit ligger i skriptet, inte i HTML:en. Det är ingen säkerhet – vem som helst kan öppna filen och läsa svaren – men det håller HTML:en ren, och ett quiz på en skolsajt behöver inte mer.
Utan JavaScript: frågorna syns och går att besvara, men ingenting rättas. Det är en rimlig nivå: innehållet finns, funktionen saknas.
Passar när din sajt har ett ämne besökaren kan testa sig på. Det är den modul som kräver mest eget innehåll – räkna med att skriva frågorna själv.
HTML:en du behöver
<form class="quiz">
<fieldset data-fraga="f1">
<legend>1. Frågan?</legend>
<p>
<input type="radio" id="f1a" name="f1" value="a">
<label for="f1a">Första alternativet</label>
</p>
<p>
<input type="radio" id="f1b" name="f1" value="b">
<label for="f1b">Andra alternativet</label>
</p>
</fieldset>
<p>
<button type="submit">Rätta</button>
</p>
<p class="quiz-resultat" aria-live="polite"></p>
</form>
Tre saker måste stämma: varje fieldset har ett eget data-fraga, alla knappar i samma fråga har samma name, och varje knapp har ett unikt id med en label som pekar på det.
Koden
/* ==========================================================================
Quiz med rättning – modul 6 av 6
Modulbiblioteket, Webbutveckling 1 område 6
Vad den gör: frågorna står i HTML:en som ett vanligt formulär med
alternativknappar. När besökaren trycker "Rätta" jämför skriptet svaren
med facit, märker varje fråga som rätt eller fel, och skriver ut
resultatet. Facit ligger i skriptet, inte i HTML:en.
Utan JavaScript: frågorna syns och går att besvara, men ingenting rättas –
knappen gör inget. Det är en rimlig nivå: innehållet finns, funktionen
saknas.
Kräver i HTML:
<form class="quiz">
<fieldset data-fraga="f1">
<legend>Frågan?</legend>
<p><input type="radio" id="f1a" name="f1" value="a">
<label for="f1a">…</label></p>
…
</fieldset>
…
<p><button type="submit">Rätta</button></p>
<p class="quiz-resultat" aria-live="polite"></p>
</form>
Kräver i CSS: avsnittet "13f. Quizet" i stil.css.
======================================================================= */
(function () {
"use strict";
/* ===== ÄNDRA HÄR ====================================================== */
const QUIZ = ".quiz";
// Facit. Nyckeln är fieldsetets data-fraga, värdet är value på det rätta
// alternativet. Lägger du till en fråga i HTML:en lägger du till en rad här.
const FACIT = {
f1: "b",
f2: "c",
f3: "a",
f4: "c",
f5: "b"
};
const TEXT_RESULTAT = "Du fick {ratt} av {antal} rätt.";
const TEXT_ALLA_RATT = "Alla rätt. Du kan reservatet.";
const TEXT_OBESVARAD = "Du har inte svarat på alla. Kolla fråga {nr}.";
const TEXT_RATT = "Rätt";
const TEXT_FEL = "Fel";
/* ===== SLUT PÅ ÄNDRA HÄR ============================================== */
const quiz = document.querySelector(QUIZ);
if (!quiz) { return; }
const fragor = quiz.querySelectorAll("fieldset[data-fraga]");
const resultat = quiz.querySelector(".quiz-resultat");
if (fragor.length === 0 || !resultat) { return; }
// Varje fråga får ett litet element där "Rätt" eller "Fel" skrivs. Det
// skapas här, en gång, så att rättningen sedan bara byter text.
fragor.forEach(function (fraga) {
const domare = document.createElement("p");
domare.className = "quiz-domare";
fraga.appendChild(domare);
});
// Rätta: gå igenom frågorna, läs det ikryssade alternativet och jämför
// med facit. Klasserna quiz-ratt och quiz-fel är det CSS:en färgar på.
function ratta() {
let antalRatt = 0;
for (let i = 0; i < fragor.length; i++) {
const fraga = fragor[i];
const nyckel = fraga.dataset.fraga;
const valt = fraga.querySelector("input:checked");
const domare = fraga.querySelector(".quiz-domare");
// Obesvarad fråga: avbryt, säg vilken, och lägg fokus där.
if (!valt) {
resultat.textContent = TEXT_OBESVARAD.replace("{nr}", i + 1);
fraga.querySelector("input").focus();
return;
}
const arRatt = valt.value === FACIT[nyckel];
fraga.classList.toggle("quiz-ratt", arRatt);
fraga.classList.toggle("quiz-fel", !arRatt);
domare.textContent = arRatt ? TEXT_RATT : TEXT_FEL;
if (arRatt) { antalRatt++; }
}
if (antalRatt === fragor.length) {
resultat.textContent = TEXT_ALLA_RATT;
} else {
resultat.textContent = TEXT_RESULTAT
.replace("{ratt}", antalRatt)
.replace("{antal}", fragor.length);
}
}
// Formuläret får inte skickas – det finns ingen server att skicka till.
// preventDefault stoppar sändningen, och sedan rättar vi i stället.
quiz.addEventListener("submit", function (handelse) {
handelse.preventDefault();
ratta();
});
})();
Ändra här
| Rad | Vad du byter till |
|---|---|
QUIZ | Klassen på ditt formulär |
FACIT | En rad per fråga: f1: "b". Nyckeln är data-fraga, värdet är value på det rätta alternativet |
TEXT_RESULTAT | {ratt} och {antal} byts ut av skriptet |
TEXT_ALLA_RATT | Vad som sägs vid full pott |
TEXT_OBESVARAD | {nr} byts mot frågans nummer |
TEXT_RATT, TEXT_FEL | Vad som står vid varje fråga efter rättningen |
Lägger du till en fråga i HTML:en måste du lägga till en rad i FACIT. Glömmer du det blir frågan alltid fel.
Skriv den själv
- Hitta formuläret, frågorna och resultatraden. Avsluta tyst om något saknas.
- Skapa ett element per fråga där rättningen ska skrivas.
- Vid sändning: stoppa sändningen, gå igenom frågorna och läs det ikryssade alternativet.
- Är en fråga obesvarad: säg vilken, flytta fokus dit och avbryt rättningen.
- Jämför med facit, märk varje fråga, räkna ihop och skriv resultatet. Resultatraden ska ha
aria-liveså att den läses upp.
Vilken ska du välja?
Minst två moduler, och de ska höra hemma på din sajt.
| Din sajt har | Välj |
|---|---|
| Fler än fyra länkar i menyn | Hamburgermeny |
| Ett mörkt färgschema från område 3 | Mörkt läge |
| En samling i tydliga kategorier | Filtrerbart galleri |
| Tre eller fler bilder som hör ihop | Bildkarusell |
| Ett formulär med flera fält | Formulärvalidering |
| Ett ämne besökaren kan testa sig på | Quiz |
Svårighetsgrad att koppla in, från lättast:
- Mörkt läge – en knapp, ingen annan HTML
- Formulärvalidering – ingen ny HTML alls, om formuläret redan är rätt byggt
- Hamburgermeny – en knapp och ett
idpå listan - Filtrerbart galleri – ett attribut per del som ska filtreras
- Bildkarusell – en struktur runt bilderna
- Quiz – kräver att du skriver frågorna
Det är inte en rangordning av betyg. Två väl inbyggda och anpassade moduler är bättre än fyra inklistrade.