Lee Froese

Web Developer

Gravity Forms Uploads with Headless WordPress

I found myself struggling through the documentation for getting file uploads in Gravity Forms in a headless WordPress setup recently. If you're not familiar with headless WordPress, it involves decoupling WordPress so you're using WordPress for it's back-end (database and content management), and a separate front-end framework (Nuxt / Next / Gatsby / etc) for the presentation layer.

1. WordPress Setup

You will need to make sure you have these plugins installed in WordPress in addition to Gravity Forms:

Once you have a form created in Gravity Forms with one or multiple file upload fields, you're ready to start on the front-end.

2. File Upload Support

The WPGraphQL Upload plugin adds upload support on the WordPress server side. It's as simple as installing the plugin in WordPress and activating it.

We also need to add upload support to the front-end using Apollo Upload Client. You can run npm install apollo-upload-client --save to add this to your Vue or React project. To use apollo-upload-client in Nuxt, you need to import the package and use the createUploadLink function in your apollo configuration for the WordPress client. Here's an example of how to do this.

//nuxt.config.js
export default {
  ...
  apollo: {
    wordpressClient: '~/queries/config.js',
  }
}
//queries/config.js
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import { createUploadLink } from 'apollo-upload-client'
import introspectionResult from '~/src/fragmentTypes.json'

const fragmentMatcher = new IntrospectionFragmentMatcher({
  introspectionQueryResultData: introspectionResult,
});

export default () => {
  return {
    link: createUploadLink({ uri: "https://domain.com/graphql"}),
    connectToDevTools: true,
    cache: new InMemoryCache({ fragmentMatcher }),
    defaultHttpLink: false,
  }
}

3. Display Your Form

In this instance, I am using Nuxt.js for the front-end. If you have multiple forms on your site, you may want to create a GForm.vue component that is flexible and can be re-used to display any Gravity Forms form on your site. I can't take credit for creating this component, that was done by Nick.

First, we need to make our call to the GraphQL WordPress endpoint and get the form data using the gfForm query.

# form.gql
query form($id: ID!) {
  gfForm(id: $id, idType: DATABASE_ID) {
    id
    title
    button {
      text
    }
    confirmations {
      message
      type
      id
    }
    formFields {
      nodes {
        id
        type
        ... on TextField {
          id
          label
        }
        ... on WebsiteField {
          id
          label
        }
        ... on EmailField {
          id
          label
        }
        ... on TextAreaField {
          id
          label
        }
        ... on SelectField {
          id
          label
          choices {
            value
          }
        }
        ... on CheckboxField {
          id
          label
          choices {
            isSelected
            text
            value
          }
        }
        ... on RadioField {
          id
          label
          choices {
            isSelected
            text
            value
          }
        }
        ... on FileUploadField {
          id
          label
        }
        ... on SectionField {
          id
          label
        }
      }
    }
  }
}

Next, we can create our GForm.vue component that displays the form.

<!-- GForm.vue -->
<template>
   <form v-on:submit.prevent="submitForm" class="contact-form [ flex flex-wrap ]" :data-id="formid">
      <slot v-for="(field, index) in data.gfForm.formFields.nodes">

        <div v-if="field.type === 'SECTION'" :key="field.id">{{ field.label }}</div>

        <div v-if="field.type === 'TEXT'" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
          <div class="field" :data-id="field.id" :data-type="field.type">
            <input :id="`field-${index}`" type="text" required />
          </div>
        </div>

        <div v-if="field.type === 'EMAIL'" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
          <div class="field" :data-id="field.id" :data-type="field.type">
            <input :id="`field-${index}`"  type="email" required />
          </div>
        </div>

        <div v-if="field.type === 'WEBSITE'" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
          <div class="field" :data-id="field.id" :data-type="field.type">
            <input :id="`field-${index}`"  type="url" required />
          </div>
        </div>

        <div v-if="field.type === 'CHECKBOX'" :key="field.id">
          <label>{{ field.label }}</label>
          <div class="field checkbox-field" :data-id="field.id" :data-type="field.type">
            <label v-for="(choice, index) in field.choices" :key="index">
              <input type="checkbox" @change="checkboxSelected" :value="choice.value" :name="field.type+field.id" />
              <span v-html="choice.value" />
            </label>
          </div>
        </div>

        <div v-if="field.type === 'RADIO'" :key="field.id">
          <label>{{ field.label }}</label>
          <div class="field radio-field" :data-id="field.id" :data-type="field.type">
            <label v-for="(choice, index) in field.choices" :key="index">
              <input type="radio" @change="radioSelected" :value="choice.value" :name="field.type+field.id" />
              <span v-html="choice.value" />
            </label>
          </div>
        </div>

        <div v-if="field.type === 'SELECT'" class="field-wrap [ w-full lg:w-1/2 ]" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
            <div class="field" :data-id="field.id" :data-type="field.type">
            <select :id="`field-${index}`">
              <option v-for="(choice, index) in field.choices" :key="index">
                {{ choice.value }}
              </option>
            </select>
          </div>
        </div>

        <div v-if="field.type === 'FILEUPLOAD'" class="field-wrap [ w-full lg:w-1/2 ]" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
          <div class="field" :data-id="field.id" :data-type="field.type">
            <input :id="`field-${index}`" type="file" />
          </div>
        </div>

        <div v-if="field.type === 'TEXTAREA'" class="field-wrap [ w-full ]" :key="field.id">
          <label :for="`field-${index}`">{{ field.label }}</label>
          <div class="field" :data-id="field.id" :data-type="field.type">
            <textarea :id="`field-${index}`" required></textarea>
          </div>
        </div>

      </slot>

      <div class="field-wrap">
        <button data-label="Send" class="btn">
          <span class="btn-label">Send</span>
        </button>
      </div>

      <template v-if="data.gfForm.confirmations">
        <slot v-for="(msg) in data.gfForm.confirmations">
          <div v-if="msg.type === 'MESSAGE'" :key="msg.id" class="message invisible" v-html="msg.message"></div>
        </slot>
      </template>

    </form>
</template>

Lastly, we can now use our form component in pages like so.

<!-- contact.vue -->
<template>
  <ApolloQuery :query="require('~/queries/forms/form.gql')" :variables="{ id: item.formId }">
    <template v-slot="{ result: { loading, error, data} }">
      <g-form v-if="data" :data="data" :formid="item.formId" />
    </template>
  </ApolloQuery>
</template>

4. Submitting the Form

To submit the form, we need to package up the field values and send it to a GraphQL mutation. Lets start with the GraphQL mutation submitGfForm. We define the input as a variable to allow us to add this data dynamically.

# submit-form.gql
mutation submitForm($input: SubmitGfFormInput!) {
  submitGfForm(input: $input) {
    confirmation {
      type
      message
      url
    }
    errors {
      id
      message
    }
  }
}

In our GForm.vue component, we have a method called submitForm that is called when a user submits the form. This method compiles all of the field values and calls the WordPress API using Apollo.

// import gql mutation
import SubmitForm from "~/queries/mutations/forms/submit-form.gql";

export default {
  ...
  methods: {
    submitForm: function(e) {
      e.preventDefault()

      // Build Input for mutation
      let gravityInput = new Object
      gravityInput['id'] = Number(e.target.dataset.id)
      gravityInput['fieldValues'] = []
      gravityInput['saveAsDraft'] = false

      // Loop through fields and set field values for mutation
      let fields = e.target.querySelectorAll('.field')
      fields.forEach(field => {

        const fieldType = field.dataset.type.toLowerCase()
        let gravityValues = {}

        // Include ID data for fields other than the file upload (we don't want this for blank file upload fields as it casues a json error)
        if(fieldType != 'fileupload') {
          gravityValues['id'] = Number(field.dataset.id)
        }

        // Set the value within the object based on type
        if(fieldType == 'email') {
          gravityValues['emailValues'] = {}
          gravityValues['emailValues']['value'] = field.firstChild.value
        }
        else if(fieldType == 'select') {
          let select = field.children.item(0)
          select.options.forEach((choice, index) => {
            if (choice.selected) {
              gravityValues['value'] = choice.value;
            }
          })
        }
        else if(fieldType == 'radio') {
          let selectedChoices = field.querySelectorAll('input:checked')
          selectedChoices.forEach((choice, index) => {
            gravityValues['value'] = choice.value
          })
        }
        else if(fieldType == 'checkbox') {
          let selectedChoices = field.querySelectorAll('input:checked')
          let checkboxValues = []
          selectedChoices.forEach((choice, index) => {
            let checkboxValue = {}
            let checkboxIdPrefix = field.dataset.id
            let checkboxIdSuffix = index+1
            let checkboxId = checkboxIdPrefix + '.' + checkboxIdSuffix
            checkboxValue['inputId'] = Number(checkboxId)
            checkboxValue['value'] = choice.value
            checkboxValues.push(checkboxValue)
          })
          gravityValues['checkboxValues'] = checkboxValues;
        }
        else if(fieldType == 'fileupload') {
          if( field.firstChild.files.length > 0) {
            gravityValues['id'] = Number(field.dataset.id)
            gravityValues['fileUploadValues'] = field.firstChild.files
          }
        }
        else {
          gravityValues['value'] = field.firstChild.value;
        }

        // Push to fieldsValues array if non empty object
        if( gravityValues && Object.keys(gravityValues).length != 0 ) {
          gravityInput['fieldValues'].push(gravityValues)
        }
      })

      // send data to GForms entries
      this.$apollo.mutate({
        mutation: SubmitForm,
        variables: {
          input: gravityInput
        }
      }).then(response => {
        // Do stuff like displaying messages here
        console.log(response)
      }).catch((error) => {
        // Error
        console.error(error)
      })
    }
  }
}

Wrap Up

You should now see file uploads in your Gravity Forms entries! You'll want to add some form of validation to your form, but this basic example should get you able to submit form data to Gravity Forms as entries.