Announcing the official GraphCMS integration for Vercel!
We've just rolled out our official GraphCMS - Vercel integration!
August 18, 2020
Let's face it, forms are everywhere across the web, and they often take significant time to build depending on the requirements.
In this tutorial we will dynamically build pages with forms using Next.js, and GraphQL.
Chapters:
Before we dive into creating our schema, let's first think about what we're going to need to enable our marketing team to spin up landing page forms from just using the CMS.
It all starts with a Page. Pages must have a slug
field so we can easily look up content from the params of any request.
Next, for simplicity, each page will have an associated Form
model. For the sake of this tutorial, we'll pick 4 form field types;
If we think of a traditional form, let's try and replace all of the data points we need to recreate a simple contact form like the following:
<form><div><label for="name">Name</label><input type="text" id="name" placeholder="Your name" required /></div><div><label for="email">Email</label><input type="email" id="email" placeholder="Your email" required /></div><div><label for="tel">Tel</label><input type="tel" id="tel" placeholder="Your contact no." /></div><div><label for="favFramework">What's your favorite framework?</label><select id="favFramework"><option value="react">React</option><option value="vue">Vue</option><option value="angular">Angular</option><option value="svelte">Svelte</option></select></div><div><label for="message">Message</label><textarea id="message" placeholder="Leave a message" /></div><div><label for="terms"><input id="terms" type="checkbox" />I agree to the terms and privacy policy.</label></div><div><button type="submit">Submit</button></div></form>
In the above form, we have some <input />
's that are required, some which are of type email
, tel
and text
, while the <select />
has no placeholder or is required.
GraphCMS has support for GraphQL Union Types. This means we can define models for each of our form field types, and associate them to our Form
model as one "has many" field.
Our schema will end up looking a little something like the following...
Page
Form
Form
Page
valuesFormInput
, FormTextarea
, FormSelect
and FormCheckbox
valuesFormInput
FormInputType
dropdownForm
FormTextarea
Form
FormSelect
FormOption
valuesForm
FormOption
FormSelect
FormCheckbox
Name, String, Single line text, and used as a Title
Form
FormInputType
valuesEMAIL
TEXT
TEL
🖐 You could add more, but it's not required for this tutorial.
Now we have an idea of how our content model looks like. Let's create the models and their associations with eachother inside GraphCMS.
You'll need an account to continue. Sign up or head to the Dashboard.
Asset
model.So that we are able to query, and build our forms, we're going to need some content inside our models.
title
and slug
. I'll call use Contact Us
, and contact
, respectively.Form
, click Create and add a new form.Form
content editor, click on Create and add a new document.FormInput
content editor, enter a name
, type
, label
and placeholder
for your form field. I'll add the values Name
, TEXT
, Your name
, Name
and set required to true
.Repeat steps 5-8 to add additional fields.
🖐 To follow along with the rest of this tutorial, I will be using the following values for my fields...
FormInput
'sName
name
TEXT
Name
Your name
true
email
EMAIL
Email
Your email
true
tel
TEL
Tel
Your contact no.
false
FormTextarea
message
Message
Leave a message
true
FormCheckbox
terms
I agree to the terms and privacy policy.
true
FormSelect
The FormSelect
is a little special because it also references another model FormSelect
.
First, create your FormSelect
document as usual, entering the following.
favFramework
What's your favorite frontend framework?
false
Now for each of our choices below, repeat the steps to "Create and add a new formOption", and provide the value
/option
for each:
react
/React
vue
/Vue
angular
/Angular
svelte
/Svelte
Finally, click Save and publish on this and close each of the inline editors, making sure to publish any unsaved changes along the way.
Now we have created our fields, we can now reorder them using the content editor. This may be useful if you decide to add or remove some fields later, you can order the fields exactly the way you want them to appear.
✨ Simply drag each of the Field rows into the order you want. ✨
We have two pages, with two separate forms:
Let's start by querying for all pages and their forms using the API Playground available from the sidebar within your project Dashboard.
__typename
{pages {titleslugform {idfields {__typename}}}}
As we're using Union Types for our form fields
, we must use the ... on TypeName
notation to query each of our models.
Let's go ahead and query on
all of our models we created earlier.
{pages {titleslugform {idfields {__typename... on FormInput {nametypeinputLabel: labelplaceholderrequired}... on FormTextarea {nametextareaLabel: labelplaceholderrequired}... on FormCheckbox {namecheckboxLabel: labelrequired}... on FormSelect {nameselectLabel: labeloptions {valueoption}required}}}}}
The response should look a little something like the following:
{"data": {"pages": [{"title": "Contact us","slug": "contact","form": {"id": "ckb9j9y3k004i0149ypzxop4r","fields": [{"__typename": "FormInput","name": "Name","type": "TEXT","inputLabel": "Name","placeholder": "Your name","required": true},{"__typename": "FormInput","name": "Email","type": "EMAIL","inputLabel": "Email address","placeholder": "you@example.com","required": true},{"__typename": "FormInput","name": "Tel","type": "TEL","inputLabel": "Phone no.","placeholder": "Your phone number","required": false},{"__typename": "FormSelect","name": "favFramework","selectLabel": "What's your favorite frontend framework?","options": [{"value": "React","option": "React"},{"value": "Vue","option": "Vue"},{"value": "Angular","option": "Angular"},{"value": "Svelte","option": "Svelte"}],"required": false},{"__typename": "FormTextarea","name": "Message","textareaLabel": "Message","placeholder": "How can we help?","required": true},{"__typename": "FormCheckbox","name": "Terms","checkboxLabel": "I agree to the terms and privacy policy.","required": true}]}}]}}
GraphCMS has a flexible permissions system, which includes enabling certain user groups to do actions, and most importantly restrict who can query what data.
For the purposes of querying data to build our pages and forms, we'll enable public API queries.
To do this, go to your project Settings.
That's it! You can test this works using the API Playground and selecting Environment: master Public
from the dropdown in the section above your query/result.
🖐 Make sure to copy your API Endpoint to the clipboard. We'll need it in step 8.
Now we have our schema, and content, let's begin creating a new Next.js project with all of the dependencies we'll need to build our pages and forms.
Inside the Terminal, run the following to create a new Next.js project.
npm init next-app dynamic-graphcms-forms
When prompted, select Default starter app
from the template choices.
cd dynamic-graphcms-forms
This template will scaffold a rough folder structure following Next.js best practices.
Next, we'll install graphql-request
for making GraphQL queries via fetch.
yarn add -E graphql-request # or npm install ...
Now, if you run the project, you should see the default Next.js welcome page at http://localhost:3000
.
yarn dev # or npm run dev
This comes in two significant parts. First we create the routes (or "paths") and then query for the data for each page with those path params.
First up is to add some code to our Next.js application that will automatically generate pages for us. For this we will be exporting the getStaticPaths
function from a new file called [slug].js
in our pages
directory.
touch pages/[slug].js
Having a filename with square brackets may look like a typo, but rest assured this is a Next.js convention.
Inside pages/[slug].js
add the following code to get going:
export default function Index(props) {return (<pre>{JSON.stringify(props, null, 2)}</pre>)}
If you're familiar with React already, you'll notice we are destructuring props
from the Index
function. We'll be updating this later to destructure our individual page data, but for now, we'll show the props
data on each of our pages.
Inside pages/[slug].js
, let's import graphql-request
and initialize a new GraphQLClient
client.
🖐 You'll need your API Endpoint from Step 6 to continue.
import { GraphQLClient } from "graphql-request";const graphcms = new GraphQLClient("YOUR_GRAPHCMS_ENDOINT_FROM_STEP_6");
Now the graphcms
instance, we can use the request
function to send queries (with variables) to GraphCMS.
Let's start by querying for all pages, and get their slugs, inside a new exported function called getStaticPaths
.
export async function getStaticPaths() {const { pages } = await graphcms.request(`{pages {slug}}`)return {paths: pages.map(({ slug }) => ({ params: { slug } })),fallback: false}}
There's quite a bit going on above, so let's break it down...
const { pages } = await graphcms.request(`{pages {slug}}`)
Here we are making a query and destructuring the response pages
from the request. This will be similar to the results we got back in step 5.
return {paths: pages.map(({ slug }) => ({ params: { slug } })),fallback: false}
Finally inside getStaticPaths
we are returning paths
for our pages, and a fallback
. These build the dynamic paths inside the root pages
directory, and each of the slugs will become pages/[slug].js
.
The fallback
is false
in this example, but you can read more about using that here.
🖐 getStaticPaths
alone does nothing, we need to next query data for each of the pages.
Now we have programmatic paths being generated for our pages, it's now time to query the same data we did in step 5, but this time, send that data to our page.
Inside pages/[slug].js
, export the following function:
export async function getStaticProps({ params: variables }) {const { page } = await graphcms.request(`query page($slug: String!) {page(where: {slug: $slug}) {titleslugform {fields {__typename... on FormInput {nametypeinputLabel: labelplaceholderrequired}... on FormTextarea {nametextareaLabel: labelplaceholderrequired}... on FormCheckbox {namecheckboxLabel: labelrequired}... on FormSelect {nameselectLabel: labeloptions {valueoption}required}}}}}`,variables);return {props: {page,},};}
Now just like before, there's a lot going on, so let's break it down...
export async function getStaticProps({ params: variables }) {// ...}
Here we are destructuring the params
object from the request sent to our page. The params here will be what we sent in getStaticPaths
, so we'd expect to see slug
here.
🖐 As well as destructuring, we are also renaming (or reassigning) the variable params
to variables
.
const { page } = await graphcms.request(`...`, variables);return {props: {page,},};
Next we're sending the same query we did in step 5, but this time we've given the query a name page
which expects the String
variable slug
.
Once we send on our renamed params
as variables
, we return an object with our page
inside of props
.
Now all that's left to do is run our Next development server and see our response JSON on the page!
yarn dev # or npm run dev
Now you should see at http://localhost:3000/contact
the data from GraphCMS for our Page.
We are now ready to dynamically build our form using the data from GraphCMS.
The __typename
value will come in handy when rendering our form, as this will decide which component gets renderered.
Inside a new directory components
, add a Form.js
file.
mkdir componentstouch components/Form.js
In this this file, we will create the structure of our basic form, and map
through each of our fields
to return the appropreciate field.
Add the following code to components/Form.js
import * as Fields from "./FormFields";export default function Form({ fields }) {if (!fields) return null;return (<form>{fields.map(({ __typename, ...field }, index) => {const Field = Fields[__typename];if (!Field) return null;return <Field key={index} {...field} />;})}<button type="submit">Submit</button></form>);}
Once you have this component setup, now create the file components/FormFields/index.js
and add the following:
export { default as FormCheckbox } from "./FormCheckbox";export { default as FormInput } from "./FormInput";export { default as FormSelect } from "./FormSelect";export { default as FormTextarea } from "./FormTextarea";
All we're doing in this file is importing each of our different form fields and exporting them.
The reason we do this is that when we import using import * as Fields
, we can grab any of the named exports by doing Fields['FormCheckbox']
, or Fields['FormInput']
like you see in components/Form.js
.
Now that that we are importing these new fields, we next need to create each of them!
For each of the imports above, create new files inside components/FormFields
for:
FormCheckbox.js
FormInput.js
FormSelect.js
FormTextarea.js
Once these are created, let's export each of the components as default, and write a minimum amount of code to make them work.
The code in the below files isn't too important. What's key about this tutorial is how we can very easily construct forms, and in fact any component or layout, using just data from the CMS. Magic! ✨
FormCheckbox.js
export default function FormCheckbox({ checkboxLabel, ...rest }) {const { name } = rest;return (<div><label htmlFor={name}><input id={name} type="checkbox" {...rest} />{checkboxLabel || name}</label></div>);}
FormInput.js
Since this component acts as a generic <input />
, we will need to lowercase the type
enumeration to pass to the input.
export default function FormInput({ inputLabel, type: enumType, ...rest }) {const { name } = rest;const type = enumType.toLowerCase();return (<div>{inputLabel && <label htmlFor={name}>{inputLabel || name}</label>}<input id={name} type={type} {...rest} /></div>);}
FormSelect.js
export default function FormSelect({ selectLabel, options, ...rest }) {const { name } = rest;if (!options) return null;return (<div><label htmlFor={name}>{selectLabel || name}</label><select id={name} {...rest}>{options.map(({ option, ...opt }, index) => (<option key={index} {...opt}>{option}</option>))}</select></div>);}
FormTextarea.js
export default function FormTextarea({ textareaLabel, ...rest }) {const { name } = rest;return (<div><label htmlFor={name}>{textareaLabel || name}</label><textarea id={name} {...rest} /></div>);}
We're done on the form components, for now...!
Let's recap...
Let's now render the form we created in step 9 to our page.
Inside pages/[slug].js
, we'll need to import our Form component and return that inside of the default export.
Below your current import (graphql-request
), import our Form component:
import Form from "../components/Form";
Lastly, update the default export to return the <Form />
.
export default function Index({ page }) {const { form } = page;return <Form {...form} />;}
Next run the Next.js development server:
yarn dev # or npm run dev
Once the server has started, head to http://localhost:3000/contact
(or a slug
you defined in the CMS) to see your form!
I'll leave the design and UI aesthetics up to you!
As far as creating dynamic forms with React, Next.js and GraphQL goes, this is it! Next we'll move onto enhancing the form to be accept submissions.
In this step we will install a library to handle our form state, and submissions, as well as create an onSubmit
that'll we'll use in Step 12 to forward onto GraphCMS.
Inside the terminal, let's install a new dependency:
yarn add -E react-hook-form # or npm install ...
Now it's not essential we use react-hook-form
for managing our form, I wanted to provide a little closer to real world scenario than your typical setState
example that are used in tutorials.
After we complete this tutorial, you should be in a position to return to each of your form fields, add some CSS, error handling, and more, made easy with react-hook-form
!
Inside components/Form.js
, add the following import to the top of the file:
import { useForm, FormContext } from "react-hook-form";
Then inside your Form
function after you return null
if there are no fields
, add the following:
const { handleSubmit, ...methods } = useForm();const onSubmit = (values) => console.log(values);
Finally, you'll need to wrap the current <form>
with <FormContext {...methods}>
, and add a onSubmit
prop to the <form>
that is onSubmit={handleSubmit(onSubmit)}
.
Your final components/Form.js
should look like this:
import { useForm, FormContext } from "react-hook-form";import * as Fields from "./FormFields";export default function Form({ fields }) {if (!fields) return null;const { handleSubmit, ...methods } = useForm();const onSubmit = (values) => console.log(values);return (<FormContext {...methods}><form onSubmit={handleSubmit(onSubmit)}>{fields.map(({ __typename, ...field }, index) => {const Field = Fields[__typename];if (!Field) return null;return <Field key={index} {...field} />;})}<button type="submit">Submit</button></form></FormContext>);}
Now all that's happening here is we're initializing a new react-hook-form
instance, and adding a FormContext
provider around our form + fields.
Next we'll need to update each of our FormFields/*.js
and register
them with the react-hook-form
context.
First update components/FormFields/FormInput.js
to include the hook useFormContext
from react-hook-form
.
At the top of the file add the following import:
import { useFormContext } from 'react-hook-form'
Then inside the FormInput
function, add the following before the return
:
const { register } = useFormContext();
Now all that's left to do add register
as a ref
to our <input />
and pass in the required
value.
<inputref={register({ required: rest.required })}id={name}type={type}{...rest}/>
The final FormInput
should look like:
import { useFormContext } from "react-hook-form";export default function FormInput({ inputLabel, type: enumType, ...rest }) {const { register } = useFormContext();const { name } = rest;const type = enumType.toLowerCase();return (<div>{inputLabel && <label htmlFor={name}>{inputLabel || name}</label>}<inputref={register({ required: rest.required })}id={name}type={type}{...rest}/></div>);}
Great! Now let's do the same for the other 3 field components:
FormCheckbox.js
import { useFormContext } from "react-hook-form";export default function FormCheckbox({ checkboxLabel, ...rest }) {const { register } = useFormContext();const { name } = rest;return (<div><label htmlFor={name}><inputref={register({ required: rest.required })}id={name}type="checkbox"{...rest}/>{checkboxLabel || name}</label></div>);}
FormSelect.js
import { useFormContext } from "react-hook-form";export default function FormSelect({ selectLabel, options, ...rest }) {if (!options) return null;const { register } = useFormContext();const { name } = rest;return (<div><label htmlFor={name}>{selectLabel || name}</label><select ref={register({ required: rest.required })} id={name} {...rest}>{options.map(({ option, ...opt }, index) => (<option key={index} {...opt}>{option}</option>))}</select></div>);}
FormTextarea.js
import { useFormContext } from "react-hook-form";export default function FormTextarea({ textareaLabel, ...rest }) {const { register } = useFormContext();const { name } = rest;return (<div><label>{textareaLabel || name}</label><textarearef={register({ required: rest.required })}htmlFor={name}id={name}{...rest}/></div>);}
🖐 Let's start the Next.js development server, and view the console when we submit the form!
yarn dev # or npm run dev
Once the server has started, head to http://localhost:3000/contact
(or a slug
you defined in the CMS) to see your form!
Open the browser developer tools console, and then fill out the form and click submit!
You should now see the form values submitted!
It's now time to take our form to the next level. We are going to update our GraphCMS schema with a new Submission
model that will be used to store submissions.
Inside the GraphCMS Schema Editor, click + Add to create a new model.
Submission
,Form Data
, and, API ID as formData
,Form
/form
, and select Form
as the Model that can be referenced,Submissions
/submissions
) respectively.Things should look a little something like the following:
And the Form
model should now have a new field submisson
:
Since we want full control via the CMS what appears on our form, we'll just save all of that data inside formData
JSON field.
🖐 Using something like webhooks would enable you to forward formData
onto a service like Zapier, and do what you need to with the data, all without writing a single line of code! ✨
In order to use the Mutations API, we'll need to configure our API access to permit mutations and create a dedicated Permanent Auth Token. Don't enable Mutations for the Public API, as anybody will be able to query/mutate your data!
Head to Settings > API Access > Permanent Auth Tokens
and create a token with the following setup:
Next, Copy
the token to the clipboard once created.
Inside of the root of your Next.js project, create the file .env
and, add the following, replacing YOUR_TOKEN_HERE
with your token:
GRAPHCMS_MUTATION_TOKEN=YOUR_TOKEN_HERE
With this token added, let's also do some housekeeping. Replace the API Endpoint you created in/pages/[slug].js
with a the .env
variable GRAPHCMS_ENDPOINT
and assign the value inside .env
:
// pages/[slug].js// ...const graphcms = new GraphQLClient(process.env.GRAPHCMS_ENDPOINT);// ...
Now before we can use the GRAPHCMS_MUTATION_TOKEN
, we'll need to update our components/Form/index.js
to POST
the values to a Next.js API route.
Inside the form, let's do a few things:
useState
from React,useState
inside your Form
function,onSubmit
function,error
after the submit <button />
import { useState } from 'react'// ...export default function Form({ fields }) {if (!fields) return null;const [success, setSuccess] = useState(null);const [error, setError] = useState(null);// ...const onSubmit = async (values) => {try {const response = await fetch("/api/submit", {method: "POST",body: JSON.stringify(values),});if (!response.ok)throw new Error(`Something went wrong submitting the form.`);setSuccess(true);} catch (err) {setError(err.message);}};if (success) return <p>Form submitted. We'll be in touch!</p>;return (// ...<button type="submit">Submit</button>{error && <span>{error}</span>}})}
Finally we'll create the API route /api/submit
that forwards requests to GraphCMS securely. We need to do this to prevent exposing our Mutation Token to the public.
One of the best ways to scaffold your mutation is to use the API Playground inside your GraphCMS project. It contains all of the documentation and types associated with your project/models.
If you've followed along so far, the following mutation is all we need to create + connect form submissions.
mutation createSubmission($formData: Json!, $formId: ID!) {createSubmission(data: {formData: $formData, form: {connect: {id: $formId}}}) {id}}
The createSubmission
mutation takes in 2 arguments; formData
and formId
.
In the onSubmit
function above, we're passing along values
which will be our formData
. All we need to do now is pass along the form ID!
We are already querying for the form id
inside pages/[slug].js
, so we can use this id
passed down to the Form
component.
Inside components/Form.js
, destructure id
when declaring the function:
export default function Form({ id, fields }) {// ...}
.... and then pass that id
into the onSubmit
body
:
const response = await fetch("/api/submit", {method: "POST",body: JSON.stringify({ id, ...values }),});
Then, inside the pages
directory, create the directory/file api/submit.js
, and add the following code:
import { GraphQLClient } from "graphql-request";export default async ({ body }, res) => {const { id, ...data } = JSON.parse(body);const graphcms = new GraphQLClient(process.env.GRAPHCMS_ENDPOINT, {headers: {authorization: `Bearer ${process.env.GRAPHCMS_MUTATION_TOKEN}`,},});try {const { createSubmission } = await graphcms.request(`mutation createSubmission($data: Json!, $id: ID!) {createSubmission(data: {formData: $data, form: {connect: {id: $id}}}) {id}}`,{data,id,});res.status(201).json(createSubmission);} catch ({ message }) {res.status(400).json({ message });}};
That's it! ✨
Now go ahead and submit the form, open the content editor and navigate to the Submission
content.
You should see your new entry!
You could use GraphCMS webhooks to listen for new submissions, and using another API route forward that onto a service of your choice, such as email, Slack or Zapier.
Now all that's left to do is deploy our Next.js site to Vercel. Next.js is buil, and managed by the Vercel team and the community.
To deploy to Vercel, you'll need to install the CLI.
npm i -g vercel # or yarn global add vercel
Once installed, all it takes to deploy is one command!
vercel # or vc
You'll next be asked to confirm whether you wish to deploy the current directory, and what the project is named, etc. The defaults should be enough to get you going! 😅
Once deployed, you'll get a URL to your site. Open the deployment URL and append /contact
to see your form!
This site uses cookies to provide you with a better user experience. For more information, refer to our Privacy Policy