En workshop for frontendprogrammet.
Målet med workshopen er å lære seg konsepter innen Javascript; DOM-en, hendelser, asynkronitet og nettverkskall. Og samtidig lage en aldri så liten Pokédex!
Klon og installer prosjektet:
git clone https://github.com/kjesvale/anvendt-javascript
cd anvendt-javascript
npm install
Prosjektet bygger med Parcel, et byggverktøy som ikke krever noen konfigurasjon, men lar oss fokusere på koden vår.
Start utviklingsserveren slik:
npm run start
Nå vil nettsiden kjøre på http://localhost:1234, og lastes på nytt hver gang du gjør endringer i src
-mappen.
Hvis det skjer noe rart under utvikling, kan du prøve å slette cachen til Parcel ved å kjøre:
npm run reset
Løs oppgavene i src
-mappen. Alle oppgavene har et løsningsforslag, som du kan avsløre ved å klikke på nøkkelsymbolet.
- ✍️ Oppgave
- 💡 Hint, tips eller triks
- 🗝 Løsningsforslag
Hvis du åpner src
-mappen, ser du at nettsiden bare består av to filer: en index.html
-side og et stilark i styles.css
. Du skal ikke gjøre noen store endringer i disse filene, for denne workshopen kommer til å fokusere på Javascript.
✍️ Lag en Javascript-fil index.js
som du importerer fra index.html
. Logg noe til konsollen, og verifiser at det logges i nettleseren.
💡 Bruk gjerne type="module"
-attributten. Dette forteller nettleseren at scriptet følger det nye modulsystemet i Javascript, og du kan bruke import
- og export
-syntaksen for å lenke mellom filer. Da er det lett å splitte opp koden i flere filer når det føles naturlig.
💡 Notat om Typescript: Parcel støtter Typescript ut av boksen, så hvis du føler deg komfortabel med det, kan du gjerne skrive Typescript i stedet for Javascript. I så fall trenger du bare å lagre filene med .ts
-endelsen i stedet for .js
. Merk at løsningsforslagene underveis er skrevet i Javascript.
Foreløbig er det ikke så mye spennende på nettsiden vår. I denne oppgaven skal vi benytte Javascript og DOM-en til å tegne opp, eller «rendre» mer innhold – dynamisk!
Hvis du åpner index.html
, ser du at siden vår inneholder en tom liste med id pokemon-list
.
✍️ Kun ved hjelp av Javascript, tegn opp følgende Pokemon i lista:
<li class="pokemon-entry">Bulbasaur</li>
🗝 Løsningsforslag
Først oppretter vi et listeelement med document.createElement
. Deretter setter vi className
-attributten og endrer den indre tekstnoden til navnet på pokemonen.
Til slutt henter vi en referanse til selve listen, og legger til listeelementet som en barnenode.
const entry = document.createElement("li");
entry.className = "pokemon-entry";
entry.innerText = "Bulbasaur";
document.getElementById("pokemon-list").appendChild(entry);
En av Pokedexens viktigste egenskap er å gi oss en beskrivelse av hver Pokemon. Utvid koden fra forrige oppgave til å tegne opp følgende HTML-struktur:
<li class="pokemon-entry">
<div class="pokemon-info">
<h2>Bulbasaur</h2>
<p>A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.</p>
</div>
</li>
🗝 Løsningsforslag
Dette likner veldig på forrige oppgave, men vi må komponere litt nøyere fordi navnet og beskrivelsen skal plasseres inni et div
-element.
// Opprett elementene med riktige attributter
const entry = document.createElement("li");
entry.className = "pokemon-entry";
const name = document.createElement("h2");
name.innerText = "Bulbasaur";
const description = document.createElement("p");
description.innerText = "It can go for days without eating a single morsel. In the bulb on its back, it stores energy.";
const info = document.createElement("div");
info.className = "pokemon-info";
// Sett alle elementene sammen
const list = document.getElementById("pokemon-list");
info.appendChild(name);
info.appendChild(description);
entry.appendChild(info);
list.appendChild(entry);
Et bilde sier mer enn tusen ord! Vi kan hente et bilde av pokemonen vår, slik at den ferdige HTML-strukturen ser slik ut:
<li class="pokemon-entry">
<div class="pokemon-info">
<h2>Bulbasaur</h2>
<p>A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.</p>
</div>
<img loading="lazy" src="https://www.serebii.net/xy/pokemon/001.png" alt="Bilde av Bulbasaur" />
</li>
✍️ Fullfør oppgaven ved å legge til et bilde under teksten.
💡 Vi bruker loading="lazy"
for å aktivere lazy loading i nettleseren. Ved å gjøre dette vil ikke bildene laste ned før man scroller dem inn i syne, og kan spare brukeren – og serveren – for unødvendig last.
🗝 Løsningsforslag
// [Koden fra 2b]
const image = document.createElement("img");
image.src = "https://www.serebii.net/xy/pokemon/001.png";
image.alt = `Bilde av Bulbasaur`;
image.loading = "lazy";
entry.appendChild(image);
Filen /assets/pokemon.json
inneholder en liste med flere pokemons på et gitt format.
✍️ Skriv en funksjon renderPokemon(pokemon, list)
med utgangspunkt i koden fra forrige oppgave. Funksjonen bør opprette et pokemon-element og legge det til i listen. Bruk funksjonen til å bygge opp en pokedex basert på JSON-listen.
💡 Du kan importere JSON-dataen med import
-syntaksen slik:
import allPokemons from "../assets/pokemon.json";
Merk at dette er en feature i byggsteget til Parcel – nettleseren ser bare JSON-dataen som en vanlig Javascript-liste.
🗝 Løsningsforslag
Her har vi generalisert koden fra forrige oppgave til en funksjon. Vi lar funksjonen tar inn et pokemon-objekt, og bruker feltene som som de heter i JSON-filen.
Vi har også laget en populateList
-funksjon som henter referansen til listen og kjører renderPokemon
-funksjonen for hver pokemon i filen.
import allPokemons from "../assets/pokemon.json";
populateList();
export function populateList() {
const list = document.getElementById("pokemon-list");
allPokemons.forEach((pokemon) => {
renderPokemon(pokemon, list);
});
}
function renderPokemon(pokemon, list) {
console.log("Rendering pokemon", pokemon.name);
// Opprett elementene med riktige attributter
const entry = document.createElement("li");
entry.className = "pokemon-entry";
const name = document.createElement("h2");
name.innerText = pokemon.name;
const description = document.createElement("p");
description.innerText = pokemon.description;
const info = document.createElement("div");
info.className = "pokemon-info";
const image = document.createElement("img");
image.src = pokemon.image;
image.alt = `Bilde av ${pokemon.name}`;
image.loading = "lazy";
// Sett alle elementene sammen
info.appendChild(name);
info.appendChild(description);
entry.appendChild(info);
entry.appendChild(image);
list.appendChild(entry);
}
Til de neste oppgavene trenger vi et API. Det er ferdiglaget, og kan startes i et nytt kommandovindu slik:
cd server
npm install
npm run start
Etter dette vil serveren kjøre på localhost:3000/api.
Nå har vi klart å tegne opp en liste med kjente pokemons. Men det er noe som mangler – en mulighet for å registrere nye pokemons! Nettsiden inneholder et komplett skjema for å registrere nye pokemons og sende dem til serveren, men dette er foreløbig skjult inni en dialog
.
Først, kopier følgende HTML-snutt inn i index.html
.
<button id="dialog-button">New pokemon</button>
Deretter,
✍️ Når brukeren trykker på knappen, åpne dialog
-elementet med showModal()
-funksjonen.
🗝 Løsningsforslag
Her henter vi referanser til knappen og dialogen, og lytter på "click"-hendelser for å bestemme når dialogen skal åpnes.
configureDialogButton();
export function configureDialogButton() {
const button = document.getElementById("dialog-button");
const dialog = document.getElementById("pokemon-dialog");
button.addEventListener("click", () => {
dialog.showModal();
});
}
Pokemonene vi registrerer vises ikke i listen, fordi denne fremdeles tegnes med data fra den statiske filen under /assets/pokemon.json
. For å se registrerte pokemons må vi hente dem fra API-et vårt.
✍️ Ved å bruke Fetch, hent pokemons fra serveren i stedet for å lese fra fil, og bruk renderPokemon
-funksjonen vi har laget fra før til å tegne dem opp i pokemonlisten.
💡 API-et har et endepunkt på localhost:3000/api/pokemon, som returnerer pokemonene som er lagret på serveren. Dette er CORS-konfigurert til å håndtere trafikk fra klienten.
🗝 Løsningsforslag
Her har vi utvidet funksjonen fra oppgave 2d) til å hente pokemons fra serveren i stedet for å lese fra fil. Vi tømmer også listen først i tilfelle vi ønsker å kjøre funksjonen flere ganger.
const list = document.getElementById("pokemon-list");
populateListFromAPI();
export async function populateListFromAPI() {
const response = await fetch("http://localhost:3000/api/pokemon");
if (response.ok) {
const pokemons = await response.json();
// Tøm listen først
list.textContent = "";
// Tegn deretter opp pokemons
pokemons.forEach((pokemon) => {
renderPokemon(pokemon, list);
});
} else {
console.log("Klarte ikke å hente pokemons fra API-et:", response.statusText);
}
}
Du kan lukke en dialog ved å trykke på Escape. Det er også anbefalt å la brukeren lukke dialogen med en egen knapp. Men det blir for lett for oss – vi ønsker å lukke dialogen når brukeren trykker utenfor dens rammer!
✍️ Lukk modalen når bruker trykker utenfor modalen.
🗝 Løsningsforslag
Dialog-elementet legger seg over alt annet og vil derfor plukke opp klikk over alt på skjermen. En mulig løsning er å lytte på klikk og sjekke om klikk-hendelsens posisjon er utenfor rammene til dialogen med .getBoundingClientRect()
.
closeDialogOnClickOutside();
export function closeDialogOnClickOutside() {
const dialog = document.getElementById("pokemon-dialog");
dialog.addEventListener("click", (event) => {
const rectangle = dialog.getBoundingClientRect();
if (
event.clientY < rectangle.top ||
event.clientY > rectangle.bottom ||
event.clientX < rectangle.left ||
event.clientX > rectangle.right
) {
dialog.close();
}
});
}
En annen løsning innebærer å wrappe innholdet til dialogen i en div
. Når brukeren klikker på dialog-elementet, sjekk om divElement.contains(event.target)
.
Nå har vi en fullt fungerende registreringsflyt for nye pokemons:
- Først henter vi alle pokemons fra serveren
- Deretter åpner vi skjemaet og fyller ut informasjonen
- Så sender vi inn skjemaet, og blir omdirigert tilbake til forsiden, hvor vi gjentar prosessen fra steg 1.
Dette fungerer, men i denne oppgaven skal vi gjøre opplevelsen enda bedre ved hjelp av Javascript.
Først skal vi hindre submit-hendelsen fra å utføre en full sidelast.
✍️ Kun ved hjelp av Javascript, stopp submit-hendelsen slik at brukeren forblir på nettsiden etter man har klikket på registreringsknappen.
🗝 Løsningsforslag
Her huker vi tak i skjemaelementet, lytter til submit-hendelsen og kjører event.preventDefault()
preventFormSubmit();
export function preventFormSubmit() {
const form = document.getElementById("pokemon-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
});
}
Vi har fjernet standardoppførselen til skjemaet, men vi har ikke implementert noe alternativ i Javascript. Derfor har vi foreløbig et skjema som ikke fungerer. La oss gjøre noe med det!
✍️ Når skjemaet submittes, gjør en POST
-request til http://localhost:3000/api/pokemon som inneholder feltene som en JSON-string. Lukk dialogen og nullstill skjemaet hvis API-et responderer positivt.
💡 Du kan trekke ut data fra et skjemaelement og konvertere det til et key/value-object slik:
const formData = new FormData(formElement);
const data = Object.fromEntries(formData);
🗝 Løsningsforslag
Her bruker vi Fetch med skjemafeltene enkodet som en JSON-string. Vi setter også på Content-Type: application/json
i headeren slik at serveren vet dataen er enkodet som JSON og ikke skjemaformat (application/x-www-form-urlencoded
).
const form = document.getElementById("pokemon-form");
const dialog = document.getElementById("pokemon-dialog");
handleFormSubmit();
export function handleFormSubmit() {
form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData(form);
const pokemon = Object.fromEntries(formData);
registerPokemon(pokemon);
});
}
async function registerPokemon(pokemon) {
const response = await fetch("http://localhost:3000/api/pokemon", {
method: "POST",
body: JSON.stringify(pokemon),
headers: {
"Content-Type": "application/json",
},
});
if (response.ok) {
window.alert(await response.text());
dialog.close();
form.reset();
}
}
Hittil har vi ennå ikke noe validering av feltene ved registrering av nye pokemons, utenom at de er tagget med required
i HTML-en.
✍️ Implementer validering i Javascript. Her er noen forslag:
- Bruk Javascript til å skru av den innebygde skjemavalideringen med novalidate-attributten, slik at den ikke spenner bein på vår egen validering.
- Når brukeren forsøker å sende inn skjemaet, valider at navn, beskrivelse og type er satt til fornuftige verdier.
- Hvis ikke, vis en feilmelding i bunn av skjemaet og fokuser på det første skjemafeltet som inneholder en feil.