SvelteKit is a fast, fun, and flexible framework built on Svelte and Vite. SvelteKit lets you write code that runs on the server for every page, including form actions, and data loaders.
In this guide we'll add the ability for readers to leave their feedback using emojis using a Form Action for any page.
If you don't already have a SvelteKit site, you can read the documentation to create one.
Let's begin by creating our GraphQL backend with the Grafbase CLI. Inside of your SvelteKit project run the following:
npx grafbase init
Then inside of the file grafbase/schema.graphql
replace the contents with the following SDL:
type Page @model {
url: URL! @unique
likes: Int @default(value: 0)
hearts: Int @default(value: 0)
poop: Int @default(value: 0)
party: Int @default(value: 0)
}
We'll use this model to track each individual page for our site, and increment the number of reactions using the atomic operations mutation.
Now run the GraphQL backend using the Grafbase CLI by running the following:
npx grafbase dev
You could easily use fetch
to make a request to Grafbase as shown in this article but we'll opt to use a thin wrapper for the purposes of this guide.
You will want to run the following command inside of your SvelteKit project:
npm install graphql graphql-request
Using graphql-request
we will import GraphQLClient
to initialize a new instance we can pass to all of the pages we want to make a GraphQL request.
For the purposes of this guide we'll add the import right inside of the +page.server.ts
file for your page. If you haven't already got a .server.ts
, add it.
Also inside of your +page.server.ts
you will want to import 2 new environment variables.
import { GRAFBASE_API_KEY, GRAFBASE_API_URL } from '$env/static/private'
import { GraphQLClient } from 'graphql-request'
const client = new GraphQLClient(GRAFBASE_API_URL, {
headers: {
'x-api-key': GRAFBASE_API_KEY,
},
})
Next create the file .env
in the root of your project and add the following:
GRAFBASE_API_URL=http://localhost:4000/graphql
GRAFBASE_API_KEY=
When working locally we can omit the value for GRAFBASE_API_KEY
. Once you deploy this to the web you will want to configure the environment variables there so they are picked up by SvelteKit and injected using $env/static/private
at build time.
The first thing we will do is fetch the current reactions from our GraphQL backend. We can do this using the client
we defined above and the following query:
const GetPageByUrl = /* GraphQL */ `
query GetPageByUrl($url: URL!) {
page(by: { url: $url }) {
...Reactions
}
}
${PageFragment}
`
You'll also want to define the custom fragment PageFragment
in your page as we'll be reusing that in two other GraphQL operations:
const PageFragment = /* GraphQL */ `
fragment Reactions on Page {
likes
hearts
poop
party
}
`
Next you will want to add the load
function, or add to it if it already exists. We'll fetch from the arguments passed to load
the url
property:
import type { PageServerLoad } from './$types'
export const load = (async ({ url }) => {
const { page } = await client.request(GetPageByUrl, {
url: url.href,
})
return {
reactions: page,
}
}) satisfies PageServerLoad
You may have noticed above that we fetch from the backend the page where the url
is of that we pass in url.href
. But if the page doesn't exist, we need to create it.
We'll start by adding the mutation to create a page by url
:
const CreatePageByUrl = /* GraphQL */ `
mutation CreatePageByUrl($input: PageCreateInput!) {
pageCreate(input: $input) {
page {
...Reactions
}
}
}
${PageFragment}
`
Then we can update the load
method to invoke that mutation:
export const load = (async ({ url }) => {
const { page } = await client.request(GetPageByUrl, {
url: url.href,
})
if (!page) {
const { pageCreate } = await client.request(CreatePageByUrl, {
input: {
url: url.href,
},
})
return { reactions: pageCreate?.page }
}
return {
reactions: page,
}
}) satisfies PageServerLoad
We're now ready to present the number of reactions inside of our page.
We'll begin by creating a very simple form to our page that just uses HTML. If you have a CSS framework configured with SvelteKit, feel free to add any styles.
We'll add 4 buttons that contain value
attributes that match the fields in our schema:
likes
hearts
poop
party
<form method="POST">
<button name="reaction" type="submit" value="likes">
👍 {data.reactions.likes}
</button>
<button name="reaction" type="submit" value="hearts">
❤️ {data.reactions.hearts}
</button>
<button name="reaction" type="submit" value="poop">
💩 {data.reactions.poop}
</button>
<button name="reaction" type="submit" value="party">
🎉 {data.reactions.party}
</button>
</form>
Once the form is added you'll notice that we're using data.reactions
. We'll need to export the data
from our server for use inside of the template.
At the top of your +page.svelte
file add the following:
<script lang="ts">
import type {PageData} from './$types'; export let data: PageData;
</script>
You should now have something that looks like this (with your own styling of course):
Now that reactions are shown on our page we will now write the action to handle form submissions.
Since we're using the value
attribute of the <button>
element we can fetch from the formData
of the request with SvelteKit and pass that onto our backend.
import type { Actions } from './$types'
type Reaction = 'likes' | 'hearts' | 'poop' | 'party'
export const actions = {
default: async ({ request, url }) => {
const data = await request.formData()
const reaction = data.get('reaction')
const { page } = await client.request(GetPageByUrl, {
url: url.href,
})
await client.request(IncrementByUrl, {
url: url.href,
input: {
[reaction as Reaction]: {
increment: 1,
},
},
})
return { success: true }
},
} satisfies Actions
You should now see when you click the buttons for each emoji that the count is updated via the form action, and the new values are shown!