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

  1. fetch (method: "GET")
  2. <template>
  3. cloneNode()
  4. 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)
async
await

GET 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

  1. fetch (method: "PUT")
  2. try/catch
  3. 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

email

radio

checkbox

tel

date

<input type="        ">

text

number

email

radio

checkbox

tel

date

Enkelt linje tekstfelt (standard)

Input type

<input type="        ">

text

number

email

radio

checkbox

tel

date

Numre/cifre

Input type

<input type="        ">

text

number

email

radio

checkbox

tel

date

E-mail

Input type

<input type="        ">

text

number

email

radio

checkbox

tel

date

Radioknap

Input type

<input type="        ">

text

number

email

radio

checkbox

tel

date

Afkrydsningsfelt

Input type

<input type="        ">

text

number

email

radio

checkbox

tel

date

Telefonnummer

Input type

<input type="        ">

text

number

email

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

Labels skal altid forbindes med deres respektive input via for-attributten og inputtets id-attribut.

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

name

Nogle 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

gap

Grupper 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

  1. fetch (method: "POST")
  2. fetch (method: "DELETE")
  3. new FormData()
  4. "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

  1. fetch (method: "POST")
  2. fetch (method: "DELETE")
  3. new FormData()
  4. "submit", "invalid", "input"

Opskrift: