API og dataflow
Fra input til gemte data
Web Dev, efterår 2026

aria-pressed="false"

state forsvinder

- Variabel (in-memory)
- sessionStorage
- localStorage
- IndexedDB
- Cookies
- Database via API
Database via API

Database via API

Database via API


Hent data fra Mockapi.io og render produktkort.
Hent og byg fra API
Øvelse
- fetch (method: "GET")
- <template>
- cloneNode()
- replaceChildren(...)
Opskrift:

GET, POST, PUT, PATCH, DELETE m.fl.
Fetch API
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json()
}function getProducts() {fetch(endpoint)asyncawaitGET er default
const response =GET, POST, PUT, PATCH, DELETE m.fl.
Fetch API
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json()
}
function getProducts() {
fetch(endpoint)
async
await
asynkront
const response =
headers
Fetch API
async function getProducts() {
const response = await fetch(endpoint, {
method: "get",
headers: {
"Content-Type": "application/json"
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json()
}api key mm.
post, delete, put
render m/ createElement()
Fetch API
async function renderData() {
const data = await getProducts()
const fragment = document.createDocumentFragment()
data.forEach(entry => {
fragment.append(createEntryElement(entry))
})
entries.replaceChildren(fragment)
}function createEntryElement(entry) {
const article = document.createElement("article")
const heading = document.createElement("h3")
const body = document.createElement("p")
article.className = "entry"
heading.textContent = entry.name
body.textContent = entry.description
article.append(heading, body)
return article
}Vi kommer til at bruge <template>
cloneNode()
Template
<template>
<article class="entry">
<h2>Overskrift-eksempel</h2>
<!-- ... -->
</article>
</template>Bliver ikke renderet
cloneNode()
Template
eksempel
Fetch + template
async function renderProducts() {
const products = await getProducts();
list.replaceChildren(...products.map(createProduct));
}
renderProducts();function createProduct(product) {
const clone = template.content.cloneNode(true);
const article = clone.querySelector(".entry");
article.querySelector("h3").textContent = product.name;
article.querySelector("p").textContent = product.description;
return clone;
}replaceChildren(elem1, elem2)Brug PUT-metoden til at opdatere favorite-status og afspejl denne i aria-pressed.
Toggle favorite
Øvelse
- fetch (method: "PUT")
- try/catch
- ariaPressed fra response
Opskrift:

PUT-metoden
Fetch API
async function toggleFavorite(id, isFavorite) {
const response = await fetch(`${endpoint}/${id}`, {
method: "put",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ favorite: isFavorite })
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json()
}PUT-metoden
Fetch API
async function handleFavoriteClick(event) {
// ...
try {
const isHearted = button.ariaPressed === "true";
const product = await toggleFavorite(id, !isHearted);
button.ariaPressed = product.favorite;
} catch (error) {
console.error("Error toggling favorite:", error);
}
}læs status fra API

PUT-metoden
Fetch API
async function handleFavoriteClick(event) {
// ...
try {
const isHearted = button.ariaPressed === "true";
const product = await toggleFavorite(id, !isHearted);
button.ariaPressed = product.favorite;
} catch (error) {
console.error("Error toggling favorite:", error);
}
}
Opdater UI
CSS
.favourite {
/*...*/
.heart-filled,
.heart-empty {
display: none;
}
&[aria-pressed="true"] {
background: var(--ink);
color: var(--accent);
}
&[aria-pressed="true"] .heart-filled,
&[aria-pressed="false"] .heart-empty {
display: block;
}
}Forms
Struktur
Eksempler

Login
Search




Facebook post

Checkout

<label>Anatomien

<label>form lingo
Anatomien

Indsaml data og send til et API-endpoint

Forms i HTML
Form

<form>
<label>Label</label>
<input>
<p>Error message</p>
<button>Submit button</button>
</form>Input type
<input type=" ">text
number
radio
checkbox
tel
date
<input type=" ">text
number
radio
checkbox
tel
date
Enkelt linje tekstfelt (standard)
Input type
<input type=" ">text
number
radio
checkbox
tel
date
Numre/cifre
Input type
<input type=" ">text
number
radio
checkbox
tel
date
Input type
<input type=" ">text
number
radio
checkbox
tel
date
Radioknap
Input type
<input type=" ">text
number
radio
checkbox
tel
date
Afkrydsningsfelt
Input type
<input type=" ">text
number
radio
checkbox
tel
date
Telefonnummer

Input type
<input type=" ">text
number
radio
checkbox
tel
date
Dato
Input type
Input type


Textarea


Textarea
<form>
<label for="comment">Kommentarer</label>
<textarea name="comment" id="comment"> </textarea>
</form>rows, cols, resize etc.
Ligesom tekst-input – bare med flere linjer
Textarea
Textarea
Select

Select
<form>
<label for="pet-select">Yndlingsdyr</label>
<select id="pet-select" name="pet">
<option value="">--Vælg et dyr--</option>
<option value="dog">Hund</option>
<option value="cat">Kat</option>
</select>
</form>Select

Dette gør feltet tilgængeligt for skærmlæsere og giver brugeren en større klik-flade, da man kan klikke på <label> for at fokusere input-feltet.
Sammenkobling
Label ↔ Input
Label/input association

<input type="checkbox" id="my-id">
<label for="my-id">Whole row</label>


Gruppér relaterede inputs
Når flere inputs hører sammen (fx radioknapper eller checkboxes), skal de pakkes i et <fieldset> med en <legend>.
Skærmlæsere forstår, at felterne hører til samme overskift.

<form>
<fieldset>
<legend>
<h3>Choose your favorite superhero</h3>
</legend>
<label for="spiderman">
<input type="radio" id="spiderman">
Spider-man
</label>
...
</fieldset>
<button>Submit</button>
</form>
Vigtig attribut
bruges til at identificere felterne i formularindsendelser
nameNogle input-typer deler også det samme navn, hvilket er vigtigt at bemærke, fordi det tillader den samme dataværdi at have flere værdier.
Øvelse
Vi bør hjælpe brugerne med usynlige krav

<div>
<label>Password</label>
<p>Password must be at least 8 characters</p>
<input />
<p>Please enter a value</p>
</div>Kobling af fejlbeskeder
Ligesom med label/input, så skal fejlbeskeden og input-feltet kobles sammen via aria-errormessage + unikt id. Og i tilfældet af fejl, så skal input også have aria-invalid="true".
<div>
<label>Password</label>
<p>Password must be at least 8 characters</p>
<input aria-errormessage="password-error" aria-invalid="true" />
<p id="password-error">Please enter a value</p>
</div>sættes via JS
Kobling af instruktioner
Instruktioner knyttes til input-feltet via aria-describedby + p.id
<div>
<label>Password</label>
<p id="password-instruction">Password must be at least 8 characters</p>
<input aria-describedby="password-instruction" />
<p id="password-error">Please enter a value</p>
</div>Form controls styling er... anderledes 😕
Et levn fra en svunden tid

...og varierer i udseende 🤦♂️



De kommer med deres egen bredde... 🤷♂️

147px (Chrome)
148px (Safari)
189px (Firefox)
CSS virker... nogle gange 🧐
Vent, hvad..?
CSS virker... nogle gange 🧐
Vent, hvad..?
Style-anbefaling
Form layout
Elementer er inline


<form>
<div class="form-group">
<label for="name">Fulde navn</label>
<input type="text" id="name" name="full-name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" />
</div>
<button>Send formular</button>
</form>Form layout
Grupper felter

<form>
<div class="form-group">
<label for="name">Fulde navn</label>
<input type="text" id="name" name="full-name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" />
</div>
<button>Send formular</button>
</form>Form layout



Grupper felter

<form>
<div class="form-group">
<label for="name">Fulde navn</label>
<input type="text" id="name" name="full-name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" />
</div>
<button>Send formular</button>
</form>Form layout



gapGrupper felter
form {
display: grid;
gap: 2rem;
}
.form-group {
display: grid;
gap: 3px;
}Form layout
<form>
<div class="form-group">
<label for="name">Fulde navn</label>
<input type="text" id="name" name="full-name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" />
</div>
<button>Send formular</button>
</form>Brug Grid
form {
display: grid;
gap: 2rem;
}
.form-group {
display: grid;
gap: 3px;
}Form layout
<form>
<div class="form-group">
<label for="name">Fulde navn</label>
<input type="text" id="name" name="full-name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" />
</div>
<button>Send formular</button>
</form>Brug Grid
Input-state (tilstandsform)

Form validation
Input-state

Vis fejlmeddelelser tæt på relevante felter og på det rigtige tidspunkt
Hvad er der galt?
Skjul fejlmeddelelser før interaktion

.error-message {
display: none;
}Input-state
"Hvis feltet ikke opfylder kriteriet, vis da fejlbesked"


Input-state
Input-state
"Hvis feltet ikke opfylder kriteriet, vis da fejlbesked"


*
*
Obligatorisk felt
"Feltet skal udfyldes"
<div>
<label>First name</label>
<input required />
<p>Please enter a value</p>
</div>Input-kriterie
"Feltet skal udfyldes, og der skal mindst være 8 tegn"
<div>
<label>Password</label>
<input minlength="8" required />
<p>Please enter a value</p>
</div>:user-valid
:user-invalid
:required
Input-state
.form-group:has(:required) {
label::after {
content: " *";
color: red;
}
}Input-state mønstre
"Hvis .form-group har et obligatorisk felt,
så tilføj en stjerne til label"

Input-state mønstre
.form-group:has(:user-invalid) {
input {
outline: 3px solid #f005;
border-color: darkred;
}
}
"Hvis .form-group har et ugyldigt felt,
så tilføj rød kant omkring input-feltet"
.form-group:has(:user-invalid) {
input {
outline: 3px solid #f005;
border-color: darkred;
}
.error-message {
display: block;
}
}
Input-state mønstre
"Hvis .form-group har et ugyldigt felt,
så vis fejlmeddelelsen"
Principle of least power
HTML -> CSS -> JavaScript -> Framework
Byg en lille workshop-tilmelding med POST.
Hent tilmeldinger fra API'et med GET, og vis dem på samme side.
Tilmeldingsformular
Øvelse
- fetch (method: "POST")
- fetch (method: "DELETE")
- new FormData()
- "submit", "invalid", "input"
Opskrift:

Form Events
const form = document.querySelector("form");
form.addEventListener( , ...);'invalid''submit'Form Events
const form = document.querySelector("form");
form.addEventListener( , ...);'invalid''submit'Form Events
const form = document.querySelector("form");
form.addEventListener( , ...);'invalid''submit'Lyt efter, om der er felter, der er ugyldige
'focusout''input'Form Events
const form = document.querySelector("form");
form.addEventListener( , ...);'invalid''submit'Lyt efter mens der tastes
'focusout''input'Form Events
const form = document.querySelector("form");
form.addEventListener( , ...);'invalid''submit''focusout'Lyt efter, når du forlader et felt
(eller 'blur')
'input'Invalid-eventet
function handleInvalid(event) {
event.preventDefault();
form.querySelector(":user-invalid").focus();
event.target.ariaInvalid = "true";
}
form.addEventListener('invalid', handleInvalid, true);Fjern browser-popup og tilføj aria-invalid
Submit-eventet
const form = document.querySelector("form");
function handleSubmit(event) {
}
form.addEventListener("submit", handleSubmit);const form = document.querySelector("form");
function handleSubmit(event) {
// 1. undgå refresh
// 2. samle data op
// 3. sende data
// 4. nustil formular
// 5. opdater UI
}
form.addEventListener("submit", handleSubmit);Submit-eventet
Hvad skal der ske, når jeg submitter?
const form = document.querySelector("form");
function handleSubmit(event) {
// 1. undgå refresh
event.preventDefault();
// 2. samle data op
// 3. sende data
// 4. nustil formular
// 5. opdater UI
}
form.addEventListener("submit", handleSubmit);Undlad browser refresh
Submit-eventet
const form = document.querySelector("form");
function handleSubmit(event) {
// 1. undgå refresh
event.preventDefault();
// 2. samle data op
// 3. sende data
// 4. nustil formular
form.reset();
// 5. opdater UI
}
form.addEventListener("submit", handleSubmit);Nulstil formularen v/ submit
Submit-eventet
const form = document.querySelector("form");
function handleSubmit(event) {
const formData = new FormData(form);
}FormData-syntaks
Saml felt-værdierne op

const form = document.querySelector("form");
function handleSubmit(event) {
const formData = new FormData(form);
console.log( formData.get("firstname") )
}.get()
Saml felt-værdierne op
Konsollen udskriver nu strengen "Peter"
<input name="firstname" />
function handleSubmit(event) {
// Hent værdier fra form
const formData = new FormData(form);
const data = {
firstname: formData.get("firstname"),
lastname: formData.get("lastname"),
email: formData.get("email")
};
}Saml felt-værdierne op
getAll() hvis flere
function handleSubmit(event) {
// Hent værdier fra form
const formData = new FormData(form);
const data = Object.fromEntries(formData);
/*const data = {
firstname: formData.get("firstname"),
lastname: formData.get("lastname"),
email: formData.get("email")
};*/
}Saml felt-værdierne op
skal matche API-feltnavn
async function handleSubmit(event) {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
const { firstname } = await addEntry(data);
statusMessage.textContent = `${firstname} was added`;
}POST
Husk try/catch
async function handleDelete(event) {
// ...
await deleteEntry(button.dataset.id);
await renderEntries();
list.focus();
}DELETE
<ul
class="entries"
tabindex="-1"
aria-labelledby="entries-heading">
</ul>async function handleDelete(event) {
// ...
await deleteEntry(button.dataset.id);
await renderEntries();
list.focus();
}DELETE
<ul
class="entries"
tabindex="-1"
aria-labelledby="entries-heading">
</ul>Byg en lille workshop-tilmelding med POST.
Hent tilmeldinger fra API'et med GET, og vis dem på samme side.
Tilmeldingsformular
Øvelse
- fetch (method: "POST")
- fetch (method: "DELETE")
- new FormData()
- "submit", "invalid", "input"
Opskrift:

Forms (webdev)
By Dannie Vinther
Forms (webdev)
Forms
- 36