Hopp til innholdet

Oppdatere et array-felt i en innholdstype i Sanity fra et React-grensesnitt

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:

Illustrasjon av innholdstyper ved relasjonsdata gjennom array-felt i Sanity. Illustrasjon: Marius Akerbæk. Kan gjengis med lenke til denne bloggartikkelen.

Å 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.

OBS: hvis du koder dette som et eksempel, husk på å koble de nye schemaene til schema.js og sørg for at de dukker opp i Sanity Studio!

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:

Illustrasjon av grensesnitt for produktside. Illustrasjon: Marius Akerbæk. Kan gjengis med lenke til denne bloggartikkelen.

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.

OBS: Dersom du allerede har aktivert credentials i CORS-origin og har et editor-token tilgjengelig i Sanity-databasen, kan du hoppe over neste steg. Dersom dette hørtes ukjent ut, sørg for at neste steg er utført.

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.

Skjermdump av grensesnittet i Sanity.io/manage hvor du setter opp CORS-origin med credentials. Illustrasjon: Marius Akerbæk

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._id 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 parameteren id. .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:

Stikkord:

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *