DEV Community

Cover image for Programmatically create forms, and capture submissions with Next.js and GraphQL
Jamie Barton for GraphCMS

Posted on • Updated on • Originally published at graphcms.com

Programmatically create forms, and capture submissions with Next.js and GraphQL

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:

  1. Define a solid content model
  2. Create the content model in GraphCMS
  3. Create an example Page and Form with Fields as a content editor
  4. Reordering form fields
  5. Query our Page, Form and Fields with GraphQL
  6. Configure public API access
  7. Setup Next.js project with dependencies
  8. Build Pages programatically with Next.js
  9. Build our Form Field components
  10. Render our Form to our individual Pages
  11. Managing form state and submissions
  12. Submitting our Form to GraphCMS with GraphQL Mutations
  13. Deploy to Vercel

TLDR;

1. Define a solid content model

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;

  • Input
  • Textarea
  • Select
  • Checkbox

Form Fields

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>
Enter fullscreen mode Exit fullscreen mode

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

Models

Page

  • Title, String, Single line text, Required, and used as a Title
  • Slug, String, Single line text, Required
  • Form, Reference to Form

Form

  • Page, Reference, Accepts multiple Page values
  • Fields, Reference, Accepts multiple FormInput, FormTextarea, FormSelect and FormCheckbox values

FormInput

  • Name, String, Single line text, and used as a Title
  • Type, Enum, FormInputType dropdown
  • Label, String, Single line text
  • Placeholder, Single line text
  • Required, Boolean
  • Form, Reference to Form

FormTextarea

  • Name, String, Single line text, and used as a Title
  • Label, String Single line text
  • Placeholder, String, Single line text
  • Required, Boolean
  • Form, Reference to Form

FormSelect

  • Name, String, Single line text, and used as a Title
  • Label, String, Single line text
  • Required, Boolean
  • Choices, Reference, Accepts multiple FormOption values
  • Form, Reference to Form

FormOption

  • Value, String, Single line text, Required, and used as a Title
  • Option, String, Single line text
  • FormSelect, Reference, Belongs to FormSelect

FormCheckbox

  • Name, String, Single line text, and used as a Title

  • Label, String, Single line text, Required

  • Required, Boolean

  • Form, Reference to Form

Enumerations

FormInputType values

  • EMAIL
  • TEXT
  • TEL

🖐 You could add more, but it's not required for this tutorial.

2. Create the models in GraphCMS

Now we have an idea of how our content model looks like. Let's create the models and their associations with eachother inside GraphCMS.

  1. You'll need an account to continue. Sign up or head to the Dashboard.

  2. Once logged in, head to the Schema editor by selecting Schema from the side.

  3. Click + Add in the sidebar above default system Asset model.

  4. Go ahead and create the 7 models above. Don't worry about creating relations just yet, you can do them all at once after creating the other fields.

3. Create an example Page and Form with Fields as a content editor

So that we are able to query, and build our forms, we're going to need some content inside our models.

  1. Inside the Dashboard, head to the Content editor by selecting Content from the side.
  2. Select the Page model and click + Create New from the top right.
  3. Give your page a title and slug. I'll call use Contact Us, and contact, respectively.
  4. Now underneath Form, click Create and add a new form.
  5. Inside the inline Form content editor, click on Create and add a new document.
  6. From the dropdown, select FormInput.
  7. Inside the inline 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.
  8. Now click Save and publish.

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

 3 x FormInput's

  • Name

    • Name: name
    • Type: TEXT
    • Label: Name
    • Placeholder: Your name
    • Required: true
  • Email

    • Name: email
    • Type: EMAIL
    • Label: Email
    • Placeholder: Your email
    • Required: true
  • Tel

    • Name: tel
    • Type: TEL
    • Label: Tel
    • Placeholder: Your contact no.
    • Required: false

1 x FormTextarea

  • Message
    • Name: message
    • Label: Message
    • Placeholder: Leave a message
    • Required: true

1 x FormCheckbox

  • Terms
    • Name: terms
    • Label: I agree to the terms and privacy policy.
    • Required: true

1 x FormSelect

The FormSelect is a little special because it also references another model FormSelect.

First, create your FormSelect document as usual, entering the following.

  • Favourite Framework
    • Name: favFramework
    • Label: What's your favorite frontend framework?
    • Required: false
  • Next below Options, click on Create and add a new formOption.

Now for each of our choices below, repeat the steps to "Create and add a new formOption", and provide the value/option for each:

  1. react/React
  2. vue/Vue
  3. angular/Angular
  4. 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.

4. Reordering form fields

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

Reordering GraphCMS Fields

5. Query our Page, Form and Fields with GraphQL

We have two pages, with two separate forms:

  • Contact Form
  • Request a Demo

Let's start by querying for all pages and their forms using the API Playground available from the sidebar within your project Dashboard.

Query pages, form and field __typename

{
  pages {
    title
    slug
    form {
      id
      fields {
        __typename
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Union Type Query

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 {
    title
    slug
    form {
      id
      fields {
        __typename
        ... on FormInput {
          name
          type
          inputLabel: label
          placeholder
          required
        }
        ... on FormTextarea {
          name
          textareaLabel: label
          placeholder
          required
        }
        ... on FormCheckbox {
          name
          checkboxLabel: label
          required
        }
        ... on FormSelect {
          name
          selectLabel: label
          options {
            value
            option
          }
          required
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
          ]
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Configure public API access

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.

  1. Open the API Access page
  2. Enable Content from stage Published under Public API permissions
  3. Save ✨

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.

API Playground Environment dropdown

🖐 Make sure to copy your API Endpoint to the clipboard. We'll need it in step 8.

7. Setup Next.js project with dependencies

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
Enter fullscreen mode Exit fullscreen mode

When prompted, select Default starter app from the template choices.

cd dynamic-graphcms-forms
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

8. Build Pages programatically with Next.js

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.

8.1 Create programmatic page routes

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

There's quite a bit going on above, so let's break it down...

const { pages } = await graphcms.request(`{
    pages {
        slug
    }
}`)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

8.2 Query page data

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}) {
        title
        slug
        form {
          fields {
            __typename
            ... on FormInput {
              name
              type
              inputLabel: label
              placeholder
              required
            }
            ... on FormTextarea {
              name
              textareaLabel: label
              placeholder
              required
            }
            ... on FormCheckbox {
              name
              checkboxLabel: label
              required
            }
            ... on FormSelect {
              name
              selectLabel: label
              options {
                value
                option
              }
              required
            }
          }
        }
      }
    }
    `,
    variables
  );

  return {
    props: {
      page,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Now just like before, there's a lot going on, so let's break it down...

export async function getStaticProps({ params: variables }) {
  // ... 
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now you should see at http://localhost:3000/contact the data from GraphCMS for our Page.

Next development server

9. Build our Form Field components

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 components
touch components/Form.js
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

 FormTextarea.js

export default function FormTextarea({ textareaLabel, ...rest }) {
  const { name } = rest;

  return (
    <div>
      <label htmlFor={name}>{textareaLabel || name}</label>
      <textarea id={name} {...rest} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We're done on the form components, for now...!

10. Render our Form to our individual Pages

Let's recap...

  • We have our form model and content coming from GraphCMS
  • We have our form fields created
  • We have our form pages automatically created

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";
Enter fullscreen mode Exit fullscreen mode

Lastly, update the default export to return the <Form />.

export default function Index({ page }) {
  const { form } = page;

  return <Form {...form} />;
}
Enter fullscreen mode Exit fullscreen mode

Next run the Next.js development server:

yarn dev # or npm run dev
Enter fullscreen mode Exit fullscreen mode

Once the server has started, head to http://localhost:3000/contact (or a slug you defined in the CMS) to see your form!

Form Components

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.

11. Managing form state and 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 ...
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Then inside the FormInput function, add the following before the return:

const { register } = useFormContext();
Enter fullscreen mode Exit fullscreen mode

Now all that's left to do add register as a ref to our <input /> and pass in the required value.

<input
  ref={register({ required: rest.required })}
  id={name}
  type={type}
  {...rest}
/>
Enter fullscreen mode Exit fullscreen mode

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>}
      <input
        ref={register({ required: rest.required })}
        id={name}
        type={type}
        {...rest}
      />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

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}>
        <input
          ref={register({ required: rest.required })}
          id={name}
          type="checkbox"
          {...rest}
        />
        {checkboxLabel || name}
      </label>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
      <textarea
        ref={register({ required: rest.required })}
        htmlFor={name}
        id={name}
        {...rest}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🖐 Let's start the Next.js development server, and view the console when we submit the form!

yarn dev # or npm run dev
Enter fullscreen mode Exit fullscreen mode

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!

Developer Tools Console

12. Submitting our Form to GraphCMS with GraphQL Mutations

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.

  • Give the model a name of Submission,
  • Add a new JSON Editor field with the Display Name Form Data, and, API ID as formData,
  • Add a new Reference field with the Display Name/API ID Form/form , and select Form as the Model that can be referenced,
  • Configure the reverse field to Allow multiple values and set the default Display Name/API ID to (Submissions/submissions) respectively.

Things should look a little something like the following:

Submission model

And the Form model should now have a new field submisson:

Form Model

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:

Create Permanent Auth Token

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
Enter fullscreen mode Exit fullscreen mode

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);
// ...
Enter fullscreen mode Exit fullscreen mode

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:

  • import useState from React,
  • Invoke useState inside your Form function,
  • Replace the onSubmit function,
  • Render 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>}}
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

API Playground

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

.... and then pass that id into the onSubmit body:

const response = await fetch("/api/submit", {
  method: "POST",
  body: JSON.stringify({ id, ...values }),
});
Enter fullscreen mode Exit fullscreen mode

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode

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!

Submission entries

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.

13. Deploy to Vercel

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
Enter fullscreen mode Exit fullscreen mode

Once installed, all it takes to deploy is one command!

vercel # or vc
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)

Collapse
dominickdesigns profile image
Dominick

Excellent! Clear and concise. Bravo! I didn't know that vercel had a CLI. The way I've been deploying is to commit to github then Vercel automatically triggers a build. Thanks for mentioning the vercel cli; I'll look into it.