Gravity Forms in Headless WordPress

Kellen Mace Avatar

·

Gravity Forms is one of the most popular premium form plugins for WordPress. It allows content creators to create a form, define which fields it should contain, render it to a page or post, and store form submissions. It also offers many extensions that provide additional features and integrations with many third-party services.

In this post, we’ll learn how we can use WPGraphQL, and WPGraphQL for Gravity Forms on a headless WordPress backend to send and receive Gravity Forms data to/from a decoupled front-end React application.

I’ll provide a suite of React components, a React hook, and helper functions that you can even drop into your projects and have Gravity Forms “just work.” They’ll handle rendering out Gravity Forms forms and sending form submissions to your headless WordPress back-end.

Let’s roll! 🏎💨

Getting Started

To benefit from this post, you should be familiar with the basics of working with the command line, local WordPress development, React, Next.js ,and Apollo Client.

Here are the steps for getting set up:

  1. Set up a local WordPress site and get it running
  2. Install and activate the Gravity Forms, WPGraphQL and WPGraphQL for Gravity Forms WordPress plugins.
  3. Clone down the Next.js app repo for this project.
  4. Import the form questionnaire form. From the WordPress admin sidebar, go to Forms > Import/Export > Import Forms. Select the gravityforms-questionnaire-form.json inside Next.js app’s folder and click the button to import it.
  5. Create an .env.local file inside of the Next.js app’s folder. Open that file in a text editor and paste in NEXT_PUBLIC_WORDPRESS_API_URL=http://wpgraphqlgravtyforms.local/graphql, replacing wpgraphqlgravtyforms.local with the domain for your local WordPress site. This is the endpoint that Apollo Client will use when it sends requests to your WordPress back-end.
  6. Run npm install or yarn to install the dependencies.
  7. Run npm run dev to get the server running locally.
  8. You should now be able to visit http://localhost:3000/ in a web browser and see the “Welcome to Next.js!” page.

How to Query for a Form

The WPGraphQL for Gravity Forms plugin provides a gravityFormsForm query that we can use to query for data about our Gravity Forms forms. Here’s a simple example:

query getForm {
   gravityFormsForm(id: 5, idType: DATABASE_ID) {
     formId
     title
     description
     formFields(first: 500) {
       nodes {
         ... on TextField {
           id
           type
           label
         }
         ... on SelectField {
           id
           type
           label
           choices {
             text
             value
           }
         }
       }
     }
   }
 }

You can see that we’re asking for some data about the form itself; namely, its formId, title and description. Then under formFields > nodes (which is a GraphQL interface), we use the ... on syntax to tell GraphQL what data we’d like back for each type of field. Any field types we include here will have data returned if they exist in the form, and any we have omitted will not– they’ll appear as empty objects instead.

We can test out this query right from the WordPress admin. Here’s how:

  1. Go to GraphQL > GraphiQL IDE.
  2. Paste the query above into the left column, replacing id: 5 with the ID of the imported form.
  3. Click the ▶ button to execute the query.
  4. See the results returned in the right column.

Check out the WPGraphQL for Gravity Forms readme for more documentation on gravityFormsForm, as well as other queries and mutations the plugin offers.

Query for a Form in our App

Now that we know what a query for a form looks like let’s see how we can use it in our front-end app.

Open up the Next.js app in a code editor, and open pages/questionnaire.tsx. Notice that we’re exporting a getStaticProps() function from this file that calls an asynchronous getGravityForm() function, passing in a form ID and returning the form data that comes back. Next.js then handles passing that form data into the Questionnaire component.

Go ahead and swap out the form ID being passed into getGravityForm() with the ID of the form you imported.

Now try navigating to http://localhost:3000/questionnaire. You should see the form’s title, description and fields displayed in all their barely-even-styled-at-all glory! ????

But where’s all this form data-fetching magic happening? Let’s dig a bit deeper. Open up utilities/gravity-forms.tsx. You’ll see that we’re defining const GET_FORM, a gql tagged template literal containing our getForm GraphQL query. It accepts formId as an argument, then fetches the form with that ID.

const GET_FORM = gql`
  query getForm($formId: ID!) {
    gravityFormsForm(id: $formId, idType: DATABASE_ID) {
      formId
      title
      description
      button {
        text
      }
      confirmations {
        isDefault
        message
      }
      formFields(first: 500) {
        nodes {
          id
          type
          ... on AddressField {
            ...AddressFieldFields
          }
          ... on CheckboxField {
            ...CheckboxFieldFields
          }
          ... on DateField {
            ...DateFieldFields
          }
          # etc.
        }
      }
    }
  }
  ${ADDRESS_FIELD_FIELDS}
  ${CHECKBOX_FIELD_FIELDS}
  ${DATE_FIELD_FIELDS}
  # etc.
`;
Code language: JavaScript (javascript)

For the formFields > nodes union, we’re making use of GraphQL fragments. The fragments are imported from other field-specific files in the project and interpolated into our gql template literal using the ${ADDRESS_FIELD_FIELDS} syntax.

These fields could have all been hardcoded in the gravity-forms.tsx file, but using GraphQL fragments allows us to co-locate the code that defines each field’s data requirements with the component responsible for rendering that field.

Lastly, check out the getGravityForm() function definition. You can see that it calls Apollo Client’s client.query() function, passing to it the getForm query and the formId as a variable. After the request finishes, it returns the form data.

Render the Form

Now that we know how the form data is being queried and passed into the Questionnaire page component, let’s see how we can use that data to render our form.

Head back over to pages/questionnaire.tsx. You’ll see that the form’s title and description render inside of h1 and p tags, respectively, and then a <GravityForm /> component is rendered, with the form passed to it as a prop:

<main>
  <h1>{title}</h1>
  <p>{description}</p>
  <GravityForm form={form} />
</main>
Code language: HTML, XML (xml)

Open components/GravityForm.tsx, where the GravityForm component is defined, and you’ll see that it renders a GravityFormsForm component, which is wrapped in a GravityFormProvider context provider:

<GravityFormProvider>
  <GravityFormsForm {...props} />
</GravityFormProvider>
Code language: HTML, XML (xml)

Don’t worry about GravityFormProvider just yet– we’ll learn about that in the next section. For now, let’s focus on the GravityFormsForm component rendered inside of it. Navigate to components/GravityFormsForm.tsx next.

GravityFormsForm accepts form as a prop and renders an HTML <form> element with the form fields, any error messages, and the submit button inside. You’ll notice that it also contains a handleSubmit function to serve as the onSubmit event handler for our form. When the form is submitted, the submitForm mutation (defined at the top of the file) executes with formId and fieldValues passed in as variables.

Next, let’s take a look at how our fields are rendering. Inside of the <form>, we map over the formFields and render a GravityFormsField component for each field:

{formFields.map(field =>
  <GravityFormsField
    key={field?.id}
    field={field as FormField}
    fieldErrors={getFieldErrors(Number(field?.id))}
  />
)}
Code language: JavaScript (javascript)

Let’s keep following this trail and head over to components/GravityFormsField.tsx to check out the GravityFormsField component. You’ll see that it’s little more than a switch() statement:

export default function GravityFormsField({ field, fieldErrors }: Props) {
  switch (field.type) {
    case "address":
      return <AddressField field={field} fieldErrors={fieldErrors} />;
    case "checkbox":
      return <CheckboxField field={field} fieldErrors={fieldErrors} />;
    case "date":
      return <DateField field={field} fieldErrors={fieldErrors} />;
    // etc.
  }
}
Code language: JavaScript (javascript)

The appropriate field component is rendered out, depending on the value of field.type. When field.type encounters an unsupported field, a “Field type not supported” message appears instead.

Render the Fields

From here, you can peruse all of the individual field components inside of the components/GravityFormsFields folder to see how they work. All of them follow the same pattern. They:

  1. Define their data requirements in a GraphQL fragment (which gets used in utilities/gravity-forms.tsx, as we saw earlier)
  2. Define the field component, which accepts these props:
    field (the data about this field)
    fieldErrors (any errors specific to this field)
    …and returns JSX for the field
  3. Use the useGravityForm() custom React hook and
    – Use its state object to get the current value for this field
    – Call its dispatch function to update the field’s value in state

“Wait, where in blazes did this custom useGravityForm() React hook come from?!”

– You, just now

I’m glad you asked! Let’s find out –

Managing our Form State

Remember earlier when we saw that GravityFormProvider context provider in components/GravityForm.tsx? That, and the custom useGravityForm() hook are defined in hooks/useGravityForm.tsx. Let’s take a look at that file next to understand better how to manage our form state.

Scroll down to GravityFormProvider. This is a React context provider that uses React’s useReducer() and passes down state and dispatch as its value:

export function GravityFormProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(reducer, DEFAULT_STATE);

  return (
    <GravityFormContext.Provider value={{ state, dispatch }}>
      {children}
    </GravityFormContext.Provider>
  );
}
Code language: JavaScript (javascript)

When dispatch() is called from inside of our field components, the reducer() function in this file determines how the form state should update. The reducer runs a switch() on the type of action that was dispatched and updates the form state accordingly using the action.fieldValue data that passed in:

switch (action.type) {
  case ACTION_TYPES.updateAddressFieldValue: {
    const { id, addressValues } = action.fieldValue as AddressFieldValue;
    return [...getOtherFieldValues(id), { id, addressValues }];
  }
  case ACTION_TYPES.updateCheckboxFieldValue: {
    const { id, checkboxValues } = action.fieldValue as CheckboxFieldValue;
    return [...getOtherFieldValues(id), { id, checkboxValues }];
  }
  // etc.
}
Code language: JavaScript (javascript)

Submitting Forms

Try submitting a few form entries yourself. After the form submits, it will be removed from the UI and replaced with the “thank you” confirmation message for our form.

Back in the WordPress admin, you can navigate to the Gravity Forms Entries page for our form and see your new entries listed. Try clicking on a few of them and confirming that the saved values match what you had entered into the form.

Error Handling

Our front-end app handles two types of errors:

Request or Server Errors

These are errors that prevent the form entry from being saved. Some examples:

  • The user’s network connection dropped out
  • The WordPress back-end threw a 500 Internal Server Error
  • The request was rejected because only logged-in users with the proper permissions are allowed to submit form entries for that particular form

If any of these errors occur, Apollo Client will return an error object. Our app will display errors at the bottom of the form if errors exist, just above the submit button.

To see this error handling in action, you can set your network connection to offline in the browser DevTools and then attempt to submit the form while offline.

Field Validation Errors

Inside the GravityFormsForm component, we check data.submitGravityFormsForm.errors to see if the server returns any validation errors. If we have field validation errors, we call the getFieldErrors() helper function to pull out the errors and pass them through to that particular field that failed validation. The error message text is displayed directly below that field, so the user knows why the field value they entered is invalid.

To test this locally, you can use the “Short Strings Only” field at the bottom of the imported form. In the Gravity Forms admin UI, we’ve configured that field to allow strings with a maximum length of 5 characters. So if you enter more than five characters into it, then try to submit the form, you’ll see the field validation error text appear below that field.

What’s Missing?

This suite of React components, custom hook, and helper function could be a starting point. You can drop them into your project and get up and running with Gravity Forms forms quickly in a React app, but there are some features they don’t provide. Some examples:

  • Client-side validation (beyond the built-in validation the browser performs on simple, single-input fields)
  • Support for Gravity Forms’ Conditional Logic rules
  • Rendering an existing Gravity Forms entry and allowing the user to update its field values
  • Support for all field types (see the section below for more details)

Which Fields are Supported?

We’re using the submitGravityFormsForm mutation that WPGraphQL for Gravity Forms provides to keep things simple. This mutation works excellent for allowing us to submit forms with some field types, such as inside of our project’s components/GravityFormsFields folder. However, it does not work for all field types that the WPGraphQL for Gravity Forms plugin supports.

If you want to use other field types, you would instead need to implement a more complex process that involves firing off multiple mutations to build a draft entry incrementally, then finally submit it. This is because currently, the GraphQL specification does not support input union types. You can learn more about that technical limitation by reading this GitHub issue thread and more about the alternative submission process in the WPGraphQL for Gravity Forms readme.

Hide Unsupported Fields in the WordPress Admin

Since not all field types support this approach, you may want to limit the Gravity Forms fields displayed in the WordPress admin to only the supported ones. This way, you’ll know that the content creators on the site can only choose fields that the headless app supports. You can do that using this plugin I wrote:

Gravity Forms Fields Allowlist

Just modify the list of ALLOWED_FIELDS inside of gravityforms-fields-allowlist.php to include the ones you want to be displayed in the WordPress admin, then install and activate the plugin.

How Can I Use This in My Projects?

  1. Copy and paste utilities/gravity-forms.tsx, hooks/useGravityForm.tsx, and all the components inside of /components into your codebase.
  2. Mimic the code in pages/questionnaire.tsx in your app; call the getGravityForm(), passing in the ID of the form, and inside of the component that receives that form prop, render the <GravityForm form={form} /> component.
  3. If you’re using TypeScript, set up GraphQL Code Generator to generate the TypeScript types stored in generated/graphql.ts and used throughout this app. A video and blog post are available that walk through this process.

Wrapping Up

Thanks for sticking with me! I hope this post and the code repos provided are helpful as you implement Gravity Forms forms in your headless WordPress + React applications. Please reach out and let us know how it goes!