Update
La oss si at vi ønsker å endre navnet på en todo. Vi har laget en edit-knapp, som foreløbig ikke gjør noenting.
User Interface (Front-end)
Scroll til HTML’en som vi returnerer. I bunn av HTML’en finner du en lukket body-tag (</body>).
På linja over den, lag en script tag. Inni denne kan vi skrive javascript for nettleseren:
<script>alert('Hello')</script>
For å holde oss organisert, ønsker vi å holde javascript for nettleseren adskilt fra server.js. Vi kan legge til et attributt som heter src i script-tag’en som gjør at vi kan laste inn en javascript-fil.
<script src="browser.js"></script>
Navnet browser.js er bare et navn som vi velger – det kunne vært pizza.js, eller unicorn.js.
De som bruker websida har ingen begrep om prosjektmappa vår, eller hva slags struktur den har, så vi må sette opp ting på en litt bestemt måte:
Lag en ny mappe i rotmappa til prosjektet, altså i mappa der server.js og package.json ligger.
Kall denne nye mappen for public. Navnet i seg selv har ingen stor betydning. Inni public-mappa, lag en ny fil som du kaller browser.js. I browser.js kan du legge til litt kode for å teste at det fungerer. Husk å lagre fila etterpå.
alert("Hello from browser.js")
Det eneste som gjenstår nå er å gjøre express oppmerksom på at denne fila finnes.
Tilbake i server.js, gå til toppen av fila. På linja rett under der vi gjør app.use(express.urlencoded):
app.use(express.static('public'))
Dette forteller app’en vår at vi skal bruke en statisk ressurs som heter public, og dette gir oss tilgang til å bruke browser.js fra app’en vår. Lagre fila, gå til nettleseren og oppdater. Hvis du fikk en alert-box har du gjort alt riktig, og vi kan nå begynne å skrive litt front-end kode til applikasjonen vår.
TIlbake i browser.js, fjern alert-linja, og legg til en klikk-lytter på document – altså hele nettsida:
document.addEventListener('click', function() {
})
Men vi ønsker egentlig å lytte til edit-knappen, ikke hele nettsida. La oss lage en parameter i vår anonyme funksjon som tar inn et event:
function(event) { // ....
I kroppen til funksjonen kan vi nå sjekke hvilket element som blir klikket på, ved bruk av if-setning:
// Hvis elementet har en HTML-klasse som heter edit-me skal koden kjøres:
if (event.target.classList.contains('edit-me')) {
alert("You clicked the edit button")
}
Lagre fila (ctrl+s), refresh nettsiden (ctrl+r eller F5). Klikk på en edit-knapp, og du bør få opp alert-box’en.
Naturligvis er det ikke en alert-box vi ønsker å vise, men nå har vi testet at det fungerer. Fjern alert-linja. Vi skal nå legge inn et element som tar inn en tekst fra brukeren, som heter prompt:
prompt('Enter your desired new text');
Lagre fila, og refresh nettleseren. Trykk på en edit-knapp, og prøv å skriv inn noe i prompt-boksen.
Dette fungerer, men teksten blir ikke lagret noe sted. Tilegn prompt-linja til en variabel:
let userInput = prompt('Enter your desired new text')
console.log(userInput) // logge ut for å sjekke at vi får forventet tilbake
Lagre fila, refresh nettleseren, og skriv inn noe tekst. Høyreklikk et sted på siden, velg inspect element, og finn javascript-konsollet, som vi har brukt tidligere. Der skal du kunne se teksten du nettopp skrev inn.
Nå trenger vi å kjøre en post-request mot vår egen nettside. JavaScript har en funksjon for dette, som heter fetch(). Fjern console.log-linja, og skriv inn følgende:
fetch(a, b).then().catch()
- Forventer en tekst-streng som inneholder URL’en vi skal sende en forespørsel til.
- Forventer et objekt, som inneholder dataene som blir sendt til serveren i argument a)
Fetch kommer til å returnere en promise; noe vi ikke har vært borti enda, og skal vente litt med å gå dypere inn på nå. Kort fortalt er en promise noe som er kjekt hvis vi kan vite hvor lang tid det kan ta før vi får en respons. then() forventer en funksjon som forteller hva som skal gjøres når vi får en respons. catch() forventer funksjon som kjører hvis fetch-funksjonen støter på problemer.
For argument a) setter vi inn ‘/update-item’. For argument b) setter vi inn et objekt som definerer noen attributter som bestemmer hva slags innhold vi forventer å få tilbake, fra vår request.
- Vi definerer en request-method som skal være POST.
- Vi definerer request-headers, som setter hva slags innholdstype vi forventer å få tilbake – vi ønsker å få tilbake JSON, som er JavaScript Object Notation.
- Vi definerer en request-body, som inneholder hvilken data vi ønsker å få tilbake. Denne må pakkes inn i en JSON.stringify()-metode. Dette er fordi selvom serveren vår vet hva text-variabelen vår er, så vet ikke browser.js noe om det – den må derfor sendes som en string.
Det ble veldig mye teknisk babbel, men her er altså koden:
document.addEventListener('click', function(event) {
if(event.target.classList.contains('edit-me')) {
let userInput = prompt('Enter your new text')
fetch('/update-item', {
method: 'POST',
headers: {'content-type': 'application/json'}
body: JSON.stringify({
"text": userInput
})
}).then(function() {
// Hva som skjer når vi får en respons
})
.catch(function() {
// Hva som skjer når noe går galt
console.log("Please try again later")
})
}
})
Lagre fila, og la oss gå tilbake til server.js. På den aller siste linja i fila, lag et nytt POST-endepunkt.
app.post(a, b)
Post tar inn to argument, a) er URL’en til det nye endepunktet vårt. Vi kaller det update-item. b) er en funksjon som blir kjørt når endepunktet blir forespurt. La oss lage en anonym funksjon som tar inn både request og response som parameter, slik at vi kan se hva vi får tilbake:
app.post('update-item', function(req, res) {
console.log(req.body.text)
res.send("Success!")
})
En siste ting før vi lagrer denne fila: gå til toppen av dokumentet, til der vi har express.urlencoded()-metoden. Den metoden gjør at vi får tilgang til skjema-data på et body-element i requesten vår. Nå ønsker vi å gjøre det samme, bare for JSON. På linja over, skriver vi:
app.use(express.json())
Lagre nå, og sjekk i terminalen at serveren er oppe og går. Gå så til nettleseren, og trykk en edit-knapp. Skriv inn tekst i feltet, og send. Gå tilbake til terminalen, og sjekk. Hvis du fikk tilbake det du skrev i terminalen, så har du gjort det riktig, og vi kan gå over til å behandle dataene vi får inn, i backend’en.
Oppdatere databasen (Back-end)
Tidligere har vi servert data fra back-end til front-end, men nå har vi altså sendt data fra front-end til back-end. Nå er det på tide å behandle dataene vi får inn, for å oppdatere databasen. Gå først helt i bunn av server.js, så vi kan jobbe med post-endepunktet for /update-item.
Fjern alt som er inni kroppen på den anonyme funksjonen, slik at vi kun står igjen med:
app.post('/update-item', function(req, res) {
})
Nå, i kroppen på funksjonen ønsker vi å snakke med databasen, bruke en collection som heter items. Denne har en metode som heter findOneAndUpdate(a, b, c). Et særdeles beskrivende navn, eller hva? Den vil finne det mongodb-objektet vi setter som argument a). Argument b) er et objekt som beskriver hva som skal oppdateres. Den må alltid begynne med{$set: {}}. I objektet setter du deretter de feltene du ønsker å oppdatere. c) argumentet tar inn en funksjon som kjøres når findOneAndReplace()-metoden har fullført.
db.collection('items').findOneAndUpdate(a, {$set: {text: req.body.text}}, function() {
res.send(‘Success’)
})
Alt vi mangler nå er a-argumentet. Hvordan skal vi knytte hver tekst som oppdateres til riktig mongodb-dokument? Som du kanskje har lagt merke til, så har hvert dokument vi har opprettet en unik ID, i tillegg til teksten som vi har lagt inn. Det er denne ID’en vi kan benytte oss av.
Gå til HTML’en i server.js, og finn ul-tag’en der vi har ${items.map}. Gå deretter ned et par linjer til du finner linja hvor vi lager en button-tag for edit. La oss legge til et attributt på denne button-tag’en, som heter data-id. Id er her et fritt valgt navn, det kunne vært hva som helst.
<button data-id="${item._id}" class="edit-me [...]
Lagre fila (ctrl+s) og last inn localhost:3000 på nytt i browseren. Høyreklikk på en edit-knapp og velg inspect element. Se til at den har nå fått attributtet, og at det dukker opp en id fra databasen.
La oss hoppe tilbake til browser.js for å passe på at front-enden også sender med id’en.
I fetch-metoden har vi et objekt som inneholder egenskapen body. På linja under eller over “text”, legg til “id. Men hva skal vi sette verdien til? Når en knapp blir trykket, får vi inn et event, som vi allerede bruker for å sjekke om inneholder klassen “edit-me”. Vi kan bruke det samme event-et til å få tak i et attributt, ved å bruke en metode som heter getAttribute():
"id": event.target.getAttribute('data-id')
Lagre fila.
Tilbake i server.js – scroll til bunn. La oss fokusere på a) argumentet.
MongoDB jobber med id’er på en litt spesiell måte, så vi kan ikke bare inkludere en string som inneholder id’en. Vi må lage et objekt som har en _id som egenskap. Som verdi, så må vi lage en ny (new) instans av ObjectId som ligger i mongodb-pakken.
.findOneAndUpdate({_id: new mongodb.ObjectId()}, b, c)
b) og c) har vi definert tidligere, så bare la de stå som de er.
Som argument til ObjectId kan vi bruke request-parameteret, se inni body, og deretter finne id:
new mongodb.ObjectId(req.body.id)
Lagre fila (ctrl+s) og oppdater nettleseren (ctrl+r eller F5). Hvis du nå endrer en av to-do’ene, vil du ikke se endringene på nettsiden. Men hvis du går til mongodb-compass, så vil du se at verdiene har blitt oppdatert. Kult!
Til slutt ønsker vi at dataene dynamisk oppdateres i front-end’en, slik at man slipper å laste inn siden på nytt for å se endringene. Gå til browser.js, og finn .then()-metoden. Siden den kjører når vi har mottatt dataene er det på tide å fylle metoden med noe hensitksmessig.
Først må vi få tak i HTML-elementet som vi skal oppdatere dynamisk.
Vi kan ta utgangspunkt i knappen som ble trykket på. To foreldre-elementer over knappen finner vi en li-tag, som inneholder en span-tag. Denne span-tag’en har en klasse som heter text-item, som vi kan få tak i. Inni then-metoden, skriv:
event.target.parentElement.parentElement.querySelector('.text-item')
Alle HTML-elementer har en egenskap som heter innerHTML som inneholder det som ligger mellom start tag’en <span> og slutt tag’en </span>. Nå som vi har tak i elementet vi ønsker kan vi bruker innerHTML for å sette en ny verdi, dynamisk. Verdien vi setter er den som brukeren har skrevet inn i feltet:
event.target.parentElement.parentElement.querySelector('.text-item').innerHTML = userInput
Lagre fila, og test ut at det funker som forventet i nettleseren.
Med det ute av veien, hva med å la brukeren få se sin nåværende tekst i redigeringsfeltet?
Vi kan legge til enda et argument i prompt-metoden for å vise frem teksten:
let userInput = prompt('Enter your desired new text', event.target.parentElement.parentElement.querySelector('.text-item').innerHTML)
Så istedenfor å sette verdien, så bare henter vi den.
Lagre fila, og prøv å redigere et par todos i nettleseren, og se at det oppdatere som det skal.
Men hva om du trykker Cancel på input-boksen? Da blir feltet under blankt. La oss ordne det, ved å sjekke at userInput faktisk finnes, før vi gjør noe annet. Rett under let userInput-linja, legg til:
if (userInput) {
}
Flytt hele fetch()-kallet (inkludert .then() og .catch()) inn i if-blokka
Meget forkortet får du noe slik:
if (userInput) {
fetch('/update-item', {...})
.then(...)
.catch(...)
}