← Område 6 Bibliotek · Interaktivitet med moduler Vilken ska du välja?

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:

DelVad du gör med den
Vad den görLäs först. Passar modulen din sajt?
HTML:en du behöverKopiera in i din sida, på rätt plats
KodenKopiera hela filen till js/ i ditt repo
Ändra härDe rader du får byta. Allt annat lämnar du
Skriv den självA-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

RadVad du byter till
KNAPPKlassen på din knapp, med punkt först
LISTAId:t på din lista, med brädgård först
TEXT_STANGDVad knappen ska säga när menyn är stängd
TEXT_OPPENVad 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.

  1. Hitta knappen och listan. Avsluta tyst om någon av dem saknas.
  2. Ta bort hidden från knappen när skriptet körs, och sätt dess text.
  3. Ett klick växlar aria-expanded mellan "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.
  4. 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

RadVad du byter till
KNAPPKlassen på din knapp
NYCKELNamnet valet sparas under. Byt till din sajts namn – annars delar två sajter på samma dator samma val
TEXT_TILL_MORKKnappens text när sidan är ljus
TEXT_TILL_LJUSKnappens 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

  1. Hitta knappen. Avsluta tyst om den saknas.
  2. Avgör vilket läge som gäller: besökarens sparade val om det finns, annars systemets (window.matchMedia("(prefers-color-scheme: dark)").matches).
  3. Sätt data-lage på document.documentElement, uppdatera knappens text och aria-pressed, och visa knappen.
  4. Ett klick byter läge, visar det nya och sparar det.
  5. All läsning och skrivning till localStorage ska ligga i try/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

RadVad du byter till
FILTERRADKlassen på din tomma div
INNEHALLSelektorn för det som ska filtreras. [data-grupp] fungerar oftast som den är
TEXT_ALLATexten på knappen som visar allt
GRUPPNAMNEn 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

  1. Hitta filterraden och allt med data-grupp. Avsluta tyst om något saknas.
  2. Samla ihop vilka grupper som finns, en gång per unikt värde, i den ordning de dyker upp.
  3. Skapa en knapp per grupp plus en "Alla" först. Den valda knappen ska ha aria-pressed="true", de andra "false".
  4. Ett klick gömmer allt som inte hör till gruppen, med hidden, och flyttar aria-pressed.
  5. 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

RadVad du byter till
KARUSELL, BILDER, STYR, FORRA, NASTA, LAGEDina klassnamn, om du inte behåller modulens
LAGETEXTTexten i lägesraden. {nr} och {antal} byts ut av skriptet
BORJA_OMtrue: 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

  1. Hitta karusellen och dess delar. Avsluta tyst om det är färre än två bilder.
  2. Visa en bild i taget med hidden och håll reda på vilken med en let.
  3. Uppdatera lägesraden. Kom ihåg att index börjar på 0 men människor räknar från 1.
  4. Knapparna och piltangenterna bläddrar. Bestäm själv vad som händer vid kanterna, och gör det till en inställning.
  5. 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

RadVad du byter till
FORMULAR"form" räcker om sidan har ett formulär. Har den flera: ge ditt ett id och skriv "#idet"
TEXT_TOMTMeddelandet för ett tomt fält. {namn} byts mot fältets etikett
TEXT_EPOSTMeddelandet för en felaktig e-postadress
TEXT_FOR_LITET, TEXT_FOR_STORTFör tal utanför min och max. {min} och {max} byts ut
TEXT_OVRIGTNä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

  1. Hitta formuläret. Sätt novalidate på det – men bara från skriptet, aldrig i HTML:en.
  2. Vid sändning: gå igenom alla fält och använd checkValidity(). Avgör vad som är fel med fältets validity-objekt.
  3. Skriv felet i ett eget element efter fältet. Elementet ska ha ett id, och fältet ska peka på det med aria-describedby och märkas med aria-invalid="true".
  4. Finns fel: stoppa sändningen och flytta fokus till det första.
  5. 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

RadVad du byter till
QUIZKlassen på ditt formulär
FACITEn 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_RATTVad som sägs vid full pott
TEXT_OBESVARAD{nr} byts mot frågans nummer
TEXT_RATT, TEXT_FELVad 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

  1. Hitta formuläret, frågorna och resultatraden. Avsluta tyst om något saknas.
  2. Skapa ett element per fråga där rättningen ska skrivas.
  3. Vid sändning: stoppa sändningen, gå igenom frågorna och läs det ikryssade alternativet.
  4. Är en fråga obesvarad: säg vilken, flytta fokus dit och avbryt rättningen.
  5. Jämför med facit, märk varje fråga, räkna ihop och skriv resultatet. Resultatraden ska ha aria-live så att den läses upp.

Vilken ska du välja?

Minst två moduler, och de ska höra hemma på din sajt.

Din sajt harVälj
Fler än fyra länkar i menynHamburgermeny
Ett mörkt färgschema från område 3Mörkt läge
En samling i tydliga kategorierFiltrerbart galleri
Tre eller fler bilder som hör ihopBildkarusell
Ett formulär med flera fältFormulärvalidering
Ett ämne besökaren kan testa sig påQuiz

Svårighetsgrad att koppla in, från lättast:

  1. Mörkt läge – en knapp, ingen annan HTML
  2. Formulärvalidering – ingen ny HTML alls, om formuläret redan är rätt byggt
  3. Hamburgermeny – en knapp och ett id på listan
  4. Filtrerbart galleri – ett attribut per del som ska filtreras
  5. Bildkarusell – en struktur runt bilderna
  6. 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.