Entry Form in Craft CMS with GraphQL and petite-vue

How to create a front-end entry form in Craft CMS using GraphQL and petite-vue.

Image

In two recent video lessons, I walked through the fun­da­men­tals of petite-vue and how to cre­ate a front-end Craft CMS entry form using vanil­la JavaScript and GraphQL. In this arti­cle, and its sib­ling video les­son, I want to com­bine the two and cre­ate a front-end entry form for Craft CMS using petite-vue and GraphQL. It’s like a franken­stein of forms…or something.

If you haven’t already reviewed how petite-vue works, then I’d encour­age you to do that first. It’s also a good idea to review the con­tact form we’re build­ing in the orig­i­nal les­son video.

Let’s createApp #

For this exam­ple, we’ll work in a file called form.js. We are not going to use a build step for this exam­ple form; we’ll just include the form.js file direct­ly in our HTML and be on our way.

<script src="/js/form.js" type="module" ></script>

At the top of the form.js file, we import the petite-vue library via import and using the ES mod­ule ver­sion of the library.

import { createApp } from "https://unpkg.com/petite-vue?module";

Now we can cre­ate the createApp() func­tion. This func­tion accepts a data object where we can define any vari­ables and func­tions we need to build our form. We’ll also define a cou­ple of func­tions exter­nal to this func­tion and call them inside of it.

import { createApp } from "https://unpkg.com/petite-vue?module";

createApp({

}).mount()

We call mount() to mount the appli­ca­tion instance and begin its life­cy­cle. We can option­al­ly explic­it­ly scope our app to an ele­ment ID in the html by pass­ing it into mount(), like mount('#contactForm'). This is nice if you have mul­ti­ple appli­ca­tions on one page. Since we only have one we will use v-scope.

Let’s scope the appli­ca­tion to just the form on the HTML page. We do that by adding v-scope to the HTML block we want petite-vue to care about.

 <form action="#" method="post" class="grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-8" id="formHandler" v-scope>

Back in our petite-vue app, let’s cre­ate a method that will han­dle pro­cess­ing the form. This is the method we’ll call when the form is submitted.

createApp({
    processForm() {
        console.log('received submission')
    }
}).mount()

To call processForm() we need to hook into the form sub­mit event. This is where petite-vue is sim­pler than JavaScript.

We add an event han­dler using @submit and then the prevent mod­i­fi­er to get the preventDefault() functionality. 

<form @submit.prevent="processForm" action="#" method="post" class="grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-8" v-scope>

Now let’s test the appli­ca­tion by sub­mit­ting the form to see if we get the mes­sage logged to the con­sole. If we do then every­thing is work­ing correctly!

If you didn’t get that mes­sage or some kind of error, then go back and review the steps again. You might have a small error but that’s okay; fix it and then keep going!

Defin­ing the Data Model

When it comes to the form data, we have a few things to consider. 

First, we need to be able to suc­cess­ful­ly cap­ture the sub­mit­ted data so we can pass it into a GraphQL request and save it in Craft CMS

Sec­ond, we need to be able to repop­u­late the forms with the sub­mit­ted data in the event that there’s an error. 

Third, we need to clear the data in all of the forms upon a suc­cess­ful submission. 

This will all be made eas­i­er by and han­dled by v-model in petite-vue, which cre­ate a two-way bind­ing of our form data.

First, let’s define our fields as an object. We’ll do this out­side of the createApp() func­tion just to keep things a bit clean­er and eas­i­er to read.

const fields = () => {
    return {
        firstName: '',
        lastName: '',
        company: '',
        email: '',
        phoneNumber: '',
        message: ''
    }
}

Here we’ve defined all of our fields with emp­ty strings as val­ues. This will make it easy to set the default when the form loads and then reset the input fields after a suc­cess­ful submission.

We can set those defaults by call­ing the func­tion in a prop­er­ty in the app.

createApp({
	fields: fields(),
    processForm() {
        console.log('received submission')
    }
}).mount()

Now we can bind those val­ues in our markup using the v-model direc­tive and then spec­i­fy which object prop­er­ty belongs to each field.

<input name="email" type="email" autocomplete="email" class="py-3 px-4 block w-full shadow-sm border-gray-500 rounded-md" required v-model="fields.email">

We do this for all of the fields using the same pat­tern. It should be: 

  • v-model="fields.firstName"
  • v-model="fields.lastName"
  • v-model="fields.company"
  • v-model="fields.email"
  • v-model="fields.phoneNumber"
  • v-model="fields.message"

If we reload the page, every­thing should look the same.

Now let’s cap­ture that form data so we can process it and save it via a GraphQL query. Inside of the processForm() method, we’ll replace the received sub­mis­sion” mes­sage with this.fields. This will log out the val­ues in fields. If all is set up prop­er­ly, then this will be the data sub­mit­ted via the form. 

There we go, it works!

Now that we have the data, we can save it to Craft CMS as an entry via GraphQL.

Sav­ing via GraphQL

The first step to sav­ing the form data via GraphQL is build our base query. This is the same query that I used in the vanil­la JavaScript ver­sion of the this form. We’ll set it to the con­st query so we can eas­i­ly access it inside of our app function. 

const query = `mutation saveContactForm($firstName: String, $lastName: String, $company: String, $phoneNumber: String, $email: String, $message: String, $authorId: ID) {
  save_submissions_contactForm_Entry(firstName: $firstName, lastName: $lastName, company: $company, phoneNumber: $phoneNumber, email: $email, message: $message authorId: $authorId) {
    firstName
    lastName
    company
    phoneNumber
    email
    message
 }
}`

That is just the query and only one part of the entire request we’ll send via GraphQL. The next part we need to build is the func­tion that preps the data and returns the entire request pay­load. We’ll call this func­tion from fetch() so it can pass in the entire pay­load based on the sub­mit­ted data.

We’re using an anony­mous func­tion assigned to the con­st payload and pass­ing in the form data from the sub­mis­sion. You’ll see in a minute where we call this func­tion and pass in the data.

This func­tion returns an object with all of the data need­ed for a full GraphQL request.

  • method: this is the method of the request; a POST request
  • head­ers: stan­dard head­ers for a GraphQL query. We’re get­ting the GraphQL token for authen­ti­ca­tion from the HTML doc­u­ment. We have it there because we grab it from the .env file in Craft CMS.
  • body: this is the body of the request and the pay­load of data that GraphQL will process. We wrap the entire thing in JSON.stringify as required by GraphQL. It includes the query (called from query con­stant we set), and then pop­u­lates each vari­able with the form data sub­mit­ted. We set authorId to 1 so Craft saves the new entry using that user as the author.
const payload = (formData) => {
    return {
        method: 'POST',
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${window.graphqlToken}`
        },
        body: JSON.stringify(
            {
                query: `${query}`,
                variables: {
                    firstName: formData.firstName,
                    lastName: formData.lastName,
                    company: formData.company,
                    phoneNumber: formData.phoneNumber,
                    email: formData.email,
                    message: formData.email,
                    authorId: 1
                }

            }
        )
    }
}
    <script>
        window.graphqlToken = "{{ getenv('GQL_CONTACT_FORM_TOKEN') }}"
        window.graphqlEndpoint = "{{ getenv(GRAPHQL_ENDPOINT') }}"
    </script>

We’ll use fetch() to make the request. Let’s build that out in our petite-vue app. It’ll go inside of the processForm() method since that’s what we’re call­ing from the HTML form via the sub­mit event handler.

Into fetch() we pass the URL of the GraphQL end­point, which we get from window.graphqlEndpoint that we set in our HTML. The val­ue is retrieved from the project’s .env file.

We also pass in the request data as the sec­ond para­me­ter. We get this data from the payload() func­tion we defined ear­li­er, and pass into it the sub­mit­ted field data. That returns JSON-encod­ed data.

If the request is suc­cess­ful, we don’t do any­thing yet (but we will in a moment). If the request fails then we log that error to the con­sole for now.

    processForm() {
        console.log(this.fields)
        fetch(window.graphqlEndpoint, payload(this.fields))
            .then(response => response.json())
            .then(response => {
				// some stuff to come here
            })
            .catch(error => {
                console.log(error)
            })
    }

Reload the page and test the form. If you have the GraphQL con­nec­tion set­up already (see my video les­son for details), then the data should sub­mit successfully.

That’s the basics of the form actu­al­ly work­ing and sub­mit­ting data. 

Reset­ting the Form

After a suc­cess­ful sub­mis­sion, we want to reset the form so it’s ready in case the user wants to sub­mit again. In the event of an unsucec­ss­ful sub­mis­sion we’ll get the val­ues re-pop­u­lat­ed auto­mat­i­cal­ly via the v‑model data binding.

We can use our fields con­stant again since it’s an object with the fields all set to emp­ty strings. We just need to call it when we want to reset the fields and the data bind­ing takes over.

We need to add a cou­ple of checks in our code to make sure we’re reset­ting the fields at the cor­rect time. Right now, we are only rely­ing on a fetch error to be caught. But we need to also check for errors in the response from GraphQL. Let’s do that here:

    processForm() {
        console.log(this.fields)
        this.processing = true
        fetch(window.graphqlEndpoint, payload(this.fields))
            .then(response => response.json())
            .then(response => {
				if(response.errors) {
					
				} else {
					this.fields = fields()
				}
                this.processing = false
            })
            .catch(error => {
                console.log(error)
            })
    }

We add a con­di­tion­al check for errors in the response JSON. If tehre aren’t any errors, we reset the fields by set­ting this.fields to fields(), which is the object with emp­ty field values.

With that done, let’s make this form nicer to use by adding some visu­al indi­ca­tors like a spin­ner and suc­cess and error messages.

Improv­ing the Form Experience

To improve the expe­ri­ence of using the form, we need to:

  • pro­vide some indi­ca­tion that is submitting
  • show a suc­cess mes­sage if the form suc­ces­ful­ly sub­mit­ted and saved a new entry
  • show an error mes­sage if there was a prob­lem with the form
  • reset the form after a suc­cess­ful submission 

This work is where we real­ly beneit from using petite-vue ver­sus vanil­la JavaScript. Nei­ther route is partcu­lar­ly dif­fi­cult, but in petite-vue we get the auto­mat­ic con­nec­tion between our app and the DOM with­out query­ing for elements.

Let’s first set a state of the form for when it’s pro­cess­ing. This will allow us to show an inde­ter­mi­nate process indi­ca­tor to inform the user that the form was indeed submittted. 

We’ll start by default­ing to false.

createApp({
    fields: fields(),
    processing: false,

We’ll set that to true inside of processForm().

processForm() {  
 console.log(this.fields)  
 this.processing = true  
 fetch(window.graphqlEndpoint, payload(this.fields))
 ...

Now we can check that pro­cess­ing boolean in our HTML via the v-if direc­tive and then show and hide a spin­ner based on it. 

In our markup, we have a com­ment­ed out block of HTML for an SVG right inside of the but­ton ele­ment. Let’s uncom­ment it and add the v-if direc­tive to it.

 <span v-if="processing">
	<svg id="processingIndicator" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
		<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
		<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
	</svg>
</span>

If processing is true, then the con­tents of the span will show. This is an ani­mat­ed spin­ner built with an SVG and some Tail­wind classes.

Now we need to set processing back to false when the form is done pro­cess­ing either suc­cess­ful­ly or unsuccessfully. 

    processForm() {
        console.log(this.fields)
        this.processing = true
        fetch(window.graphqlEndpoint, payload(this.fields))
            .then(response => response.json())
            .then(response => {
                this.processing = false
            })
            .catch(error => {
                console.log(error)
				this.processing = false
            })
    }

Let’s also add mes­sages if the sub­mis­sion was suc­cess­ful or if it was unsuc­cess­ful. Ear­li­er, we added an extra check for errors in the response JSON. If there are errors there, then we can set an errors prop­er­ty to the val­ue of true. 

We’ll first define both the errors and sub­mit­ted prop­er­ties with false values.

createApp({
    fields: fields(),
    processing: false,
    submitted: false,
    errors: false,
	...

This way the default state of our form is no errors and the form is unsub­mit­ted. Now we can change those val­ues depend­ing on the con­di­tion of the sub­mis­sion inside of fetch.

Let’s start with suc­cess­ful sub­mis­sions. If a form is sub­mit­ted suc­cess­ful­ly, then we dis­play the suc­cess noti­fi­ca­tion. This is com­ment­ed out in the HTML code. 

<div class="transition-opacity duration-500 ease-in-out sm:col-span-2">
	{{ messages.success("We received your project submission! We'll follow up in less than a day.") }}  
</div>

The mes­sage is for­mat­ted via a macro in Twig. 

We set the val­ue of submitted to true inside of the else state­ment when check­ing for errors in the response. If no errors, then we can assume it was successful. 

...
} else {  
 	this.submitted = true  
 	this.fields = fields()
...

We need to use a v-if direc­tive here to check for true so we can tog­gle that HTML block to be visible.

<div v-if="submitted" class="transition-opacity duration-500 ease-in-out sm:col-span-2">
	{{ messages.success("We received your project submission! We'll follow up in less than a day.") }}  
</div>

If the form was sucess­ful­ly sub­mit­ted, then submitted will be true and the mes­sage will show!

Now let’s do the error mes­sage the same way. We already set the default state as a prop­er­ty. We now need to update it when errors occur. For errors, this can hap­pen in two places: when the JSON has an errors prop­er­ty, and if fetch runs into an under­ly­ing error mak­ing the request.

createApp({
    fields: fields(),
    processing: false,
    submitted: false,
    errors: false,
    processForm() {
        console.log(this.fields)
        this.processing = true
        fetch(window.graphqlEndpoint, payload(this.fields))
            .then(response => response.json())
            .then(response => {
                if(response.errors) {
                    this.errors = true
                } else {
                    this.submitted = true
                    this.fields = fields()
                }
                this.processing = false
            })
            .catch(error => {
                this.errors = true
                console.log(error)
            })
    }
}).mount()

We can sim­lu­ate an error by alter­ing the GraphQL end­point or token. This will throw an error from fetch(). We can throw an error via the response from GraphQL JSON by alter­ing a field name in the query.

Now we have a func­tion­ing form using Craft CMS, GraphQL, and petite-vue!