I mange tilfeller ønsker vi å registrere informasjon fra brukere eller besøkende på en nettside, for eksempel rating og/eller kommentarer til filmer, produkter eller lignende, eller å føre timer på et prosjekt i et intranett. Datamodellene har ofte et array-felt i en innholdstype for å lagre relatert data til bestemt innhold. I Sanity vil et eksempel på slik data være et felt av felttypen Array, som kan inneholde innholdstypen reviews. Dette array-feltet finnes i innholdstypen product:
Å skrive anmeldelser rett inn i Sanity Studio i array-feltet er enkelt, men hvordan tar vi dem imot fra brukerne via grensesnittet på en nettside? Det skal vi se på nå.
Forutsetninger
Dersom du ønsker å kode dette som et testeksempel, trenger du en konto på Sanity, et Sanity-prosjekt og en React-applikasjon koblet til Sanity-prosjektet. Dersom du ikke har det, kan du gjennomføre tutorialen «Start et prosjekt med React, Sanity og Sass fra scratch» først!
Innholdstyper
I dette eksempelet tenker vi oss innholdstypene product og review, som vist i illustrasjonen over. Med den datamodellen vil Sanity-schemaet for innholdstypen product se slik ut:
export default {
name: 'product',
type: 'document',
title: 'Products',
fields: [
{
name: 'title',
type: 'string',
title: 'Product title'
},
{
name: 'description',
type: 'string',
title: 'Description'
},
{
name: 'price',
type: 'string',
title: 'Price'
},
{
name: 'image',
type: 'image',
title: 'Product image'
},
{
name: 'reviews',
type: 'array',
of: [{type: "review"}],
title: 'Reviews'
},
]
}
Code language: JSON / JSON with Comments (json)
Legg merke til feltet reviews (linje 21-26). Felttypen er array, og arrayet består av elementer av innholdstypen review. La oss nå lage schemaet til innholdstypen review:
export default {
name: 'review',
type: 'object',
title: 'Reviews',
fields: [
{
name: 'name',
type: 'string',
title: 'Name'
},
{
name: 'comment',
type: 'text',
title: 'Comment'
},
{
name: 'rating',
type: 'number',
title: 'Rating'
}
]
}
Code language: JSON / JSON with Comments (json)
Her lager vi en innholdstype som er av typen object (se linje 3). Denne vil ikke dukke opp i Sanity Studio som en egen innholdstype, men kun være tilgjengelig inne på et produkt.
Nå har vi innholdsstrukturen klar. Men hvordan tar vi imot data fra et brukergrensesnitt og setter disse inn i arrayen? La oss først lage grensesnittet som skal ta dem imot (her tar jeg forutsetningen om at utseendet stiles med CSS av dere selv).
Grensesnittet vil kunne se noe omtrent slikt ut:
OBS: Siden denne tutorialen fokuserer på å oppdatere et array-felt, kommer vi ikke til å se på componenten Review
og uthenting av data fra Sanity her.
Component: Product
//component Product
import ReviewForm from "./ReviewForm"
import Review from "./Review"
export default function Product() {
//... kode for å hente produktinfo
return (
<article className="product">
{/*HTML for produktinfo*/}
</article>
<section id="reviews">
{ /* reviews-arrayen i product-innholdstypen mappet opp: */
product.reviews.map((review, index) =>
<Review key={index} review={review} />
}
</section>
<section id="add-review">
<ReviewForm />
</section>
)
}
Code language: JavaScript (javascript)
Component: ReviewForm
//component ReviewForm
export default function ReviewForm() {
return (
<form>
<p>
<label for="name">Your name</label>
<input type="text" name="name" id="name" />
</p>
<p>
<label for="name">Your Rating</label>
<select name="rating" id="rating">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</p>
<p>
<label for="name">Your comment</label>
<textarea name="comment" id="comment"></textarea>
</p>
<p><button>Save review</button>
)
}
Code language: JavaScript (javascript)
Få ut data fra <form>
Når vi har skjemaet klart, må vi få tak i dataene når en bruker fyller ut feltene. Det finnes ulike måter å gjøre dette på, men for å gjøre det veldig oversiktlig bruker vi en state for hvert felt, og oppdaterer statene ved endringer i feltene:
//component ReviewForm
import {useState} from "react"
export default function ReviewForm() {
//Prepare states for field data:
const [name, setName] = useState(null)
const [rating, setRating] = useState(null)
const [comment, setComment] = useState(null)
//Create functions for handling data:
const handleNameChange = (e) => {
e.preventDefault()
setName(e.target.value)
}
const handleRatingChange = (e) => {
e.preventDefault()
setRating(e.target.value)
}
const handleCommentChange = (e) => {
e.preventDefault()
setComment(e.target.value)
}
return (
<form>
<p>
<label for="name">Your name</label>
<input type="text" name="name" id="name" value={name} onChange={handleNameChange} />
</p>
<p>
<label for="name">Your Rating</label>
<select name="rating" id="rating" onChange={handleRatingChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</p>
<p>
<label for="name">Your comment</label>
<textarea name="comment" id="comment" value={comment} onChange={handleCommentChange} />
</p>
<p><button>Save review</button>
)
}
Code language: JavaScript (javascript)
Med kodeoppdateringen vil endringer i skjemaet lagres i states, og være tilgjengelige når vi skal sette dem inn i Sanity. Før vi starter den jobben, må vi gjøre Sanity mulig å skrive til.
Sett opp skrivetilgang i Sanity og Sanity-klienten
Gå inn på sanity.io/manage og finn ditt prosjekt. Klikk deg inn på prosjektet, og inn på fanen «API». Her må du først tillate credentials i CORS-origin til din applikasjon.
CORS origin med credentials
Hvis du kjører react-applikasjonen lokalt, er det sannsynligvis http://localhost:3000 som er URL-en du må gi tilgang til. Husk å huke av for «Allow credentials».
OBS: Hvis du har en CORS-origin til localhost:3000 allerede UTEN credentials, slett denne og opprett en ny i henhold til steget over.
Token (tilgangsnøkkel)
Når du har satt opp CORS origin, scroll videre ned til overskriften Token. Her skal du opprette en token med editor-rettigheter, som betyr at denne tokenen (token betyr i praksis «tilgangsnøkkel») har både lese- og skriverettigheter.
OBS: Kopier tokenen til et lokalt tekstdokument så snart du har laget den. Du vil ikke få tilgang til å se denne igjen etter den er opprettet, så ta vare på den!
Client med skrivetilgang
Du har muligens en Sanity-client allerede, som ser noe ala dette ut:
//utils/sanity/client.js
import sanityClient from '@sanity/client'
const options = {
projectId: "xyz123",
dataset: "production"
}
const client = sanityClient({
...options,
apiVersion: "2021-10-21",
useCdn: true
});
Code language: JavaScript (javascript)
Denne klienten har ikke skrivetilgang. Vi lager en client til, som vi bruker i de tilfellene vi trenger skrivetilgang:
import sanityClient from '@sanity/client'
const options = {
projectId: "xyz123",
dataset: "production"
}
const client = sanityClient({
...options,
apiVersion: "2021-10-21",
useCdn: true
});
export const writeClient = sanityClient({
...options,
token: "the-token-you-created-at-sanity.io/manage",
useCdn: false
})
Code language: JavaScript (javascript)
Du må selvfølgelig bytte ut projectId
med din egen prosjektid, og token
skal være tilgangsnøkkelen du lagde i forrige steg.
OBS: Det er absolutt ikke beste praksis å la token ligge i koden slik som dette. Token er en privat tilgangsnøkkel som ikke skal deles. I dette eksempelet gjør vi det slik for enkelhet, men i et prosjekt som skal publiseres er det bedre praksis å legge denne i en .env-fil. Les mer om .env (environment)-filer i react-prosjekter her.
Nå har vi opprettet skrivetilgang til Sanity, og har en client som lar react-applikasjonen vår skrive til databasen.
Asynkron funksjonalitet for å oppdatere array-felt i Sanity
Nå skal vi lage funksjonalitet som skjer når vi trykker Lagre-knappen i et review-skjema. Denne funksjonen skal kjøre en service – eller en spørring mot Sanity-databasen som ber om å opprette et nytt element i arrayen reviews
på et produkt.
For å få til det, må vi også kjenne til ID-en til produktet vi skal oppdatere! Vi antar i koden at vi har hentet produktinformasjonen til et produkt i componenten Product
. Da sender vi med produktid-en som en prop
til ReviewForm
-componenten:
//component Product
import ReviewForm from "./ReviewForm"
import Review from "./Review"
import {useState} from "react"
export default function Product() {
const [product, setProduct] = useState(null)
//... kode for å hente produktinfo fra Sanity og lagre i state product her
return (
<article className="product">
{/*HTML for produktinfo*/}
</article>
<section id="reviews">
{ /* reviews-arrayen i product-innholdstypen mappet opp: */
product.reviews.map((review, index) =>
<Review key={index} review={review} />
}
</section>
<section id="add-review">
<ReviewForm productid={product?._id} />
</section>
)
}
Code language: JavaScript (javascript)
Under forutsetning at vi har lagret produktinformasjonen i en state kalt product
, sender vi her med product._i
d inn i ReviewForm
-componenten på linje 21. Nå må vi også oppdatere ReviewForm
til å ta imot denne propen. Deretter kan vi lage funksjonen som skal kjøre når vi sender skjemaet:
//component ReviewForm
import {useState} from "react"
export default function ReviewForm({projectid}) {
//Prepare states for field data:
const [name, setName] = useState(null)
const [rating, setRating] = useState(null)
const [comment, setComment] = useState(null)
//Create functions for handling data:
const handleNameChange = (e) => {
e.preventDefault()
setName(e.target.value)
}
const handleRatingChange = (e) => {
e.preventDefault()
setRating(e.target.value)
}
const handleCommentChange = (e) => {
e.preventDefault()
setComment(e.target.value)
}
const handleSubmit = async (e) => {
e.preventDefault()
const result = await updateReview(prosjektid,name,rating,comment)
console.log(result)
}
return (
<form>
<p>
<label for="name">Your name</label>
<input type="text" name="name" id="name" value={name} onChange={handleNameChange} />
</p>
<p>
<label for="name">Your Rating</label>
<select name="rating" id="rating" onChange={handleRatingChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</p>
<p>
<label for="name">Your comment</label>
<textarea name="comment" id="comment" value={comment} onChange={handleCommentChange} />
</p>
<p><button onClick={handleSubmit}>Save review</button>
)
}
Code language: JavaScript (javascript)
Det vil fortsatt ikke skje noe lagring når vi klikker Save review-knappen enda, siden vi prøver å kalle en service updateReview som ikke er laget enda. Men den skal vi lage nå! Forutsatt at vi har en mappe i /src
kalt utils/sanity
(der vi lagret clienten vi laget tidligere), kan vi lage en mappe services som vi oppretter en fil kalt reviewServices.js
i.
//utils/sanity/services/reviewServices.js
import {writeClient} from "../client"
export async function updateReview(id, n, r, c) {
const result = await writeClient.patch(id)
.setIfMissing({reviews: []})
.append("reviews", [{name: n, rating: r, comment: c}]
.commit({autoGenerateKeys: true})
.then(() => {return "Review added successfully!"})
.catch((err) => {return "Review save failed: " + err.message})
return result
}
Code language: JavaScript (javascript)
La oss bryte ned koden for å se hva vi gjør her:
- Vi importerer
writeClient
-en vi lagde tidligere. Dette for å ha skrivetilgang og få lov til å sende data til Sanity-databasen. - Vi lager en asynkron funksjon, en service, kalt
updateReview
som tar imot fire parametere: id (prosjektid), n (name), r (rating) og c (comment). - Vi kaller metoden
writeClient.patch()
, og sender med parameterenid
..patch
er en metode Sanity bruker for å oppdatere allerede eksisterende data. Siden produktet allerede eksisterer, og reviews lagres i et array-felt på et produkt, skal vi nå oppdatere eksisterende innhold ved å legge til mer informasjon i et felt, ikke opprette et helt nytt dokument. - Vi kaller deretter metoden
setIfMissing()
, og forteller at dersom feltet reviews ikke allerede har data, gjør vi klar en array som kan ta imot dataene vi sender nå. - Deretter kaller vi metoden
.append()
, som har som oppgave og sette data inn på slutten av en array. Vi sender med to parametere; feltnavnet og dataobjectet. Dataobjectet må ha keys som matcher schema-et til dette dataene, ergo må name-, rating- og comment-nøklene hete det samme som de tilsvarende field-names i schemaet reviews. .commit({autoGenerateKeys: true})
sørger for at alle elementer i arrayen får en unik _key, og kan dermed pekes ut for videre administrasjon, eksempelvis oppdatering eller sletting (ikke del av denne tutorialen).then(() => { //message }
kobler vi på for å returnere en melding dersom alt går bra etter vi har satt inn data..catch((err) => { //message }
brukes for å returnere en feilmelding dersom ting ikke fungerer.
Når vi klikker Save review-knappen i ReviewForm
-propen nå, kaller vi updateReview
-servicen vi nettopp har laget, og sender med dataene vi har lagret i statene i ReviewForm
. Når updateReview
-servicen kjører, ber denne Sanity om å patche, eller oppdatere, dokumentet med id-en vi har sendt med servicen. Oppdateringen er å sette et nytt element med data inn i et array-felt.
Da skal vi være i boks! Nå vil det være mulig å oppdatere et array-felt i et document i Sanity direkte fra React-grensesnittet. Du sitter igjen med en filstruktur som ser omtrent slik ut:
For mange states?
Det finnes mange mer effektive måter å ta imot data fra skjemaet på enn å lage en state for hver input. Se tutorialen How to build forms in react fra Digital Ocean for et godt eksempel på hvordan hooken useReducer kan brukes for å effektivisere uthentingen av data.
Lære mer?
Her er noen fornuftige kilder til dokumentasjon som beskriver delene vi må gjennom for å få til det vi har gjort i denne tutorialen: