Using Forms in Statamic vs. WordPress with Alpine JS
In our last post about Statamic, we introduced using web forms. Here, we will enhance them with JavaScript, so that we’re “with the times” by doing error handling, and submitting the form with AJAX and not doing a full page refresh. Not that there’s anything wrong with that.
Follow along as we explore introducing JavaScript to our Statamic form, if you’d rather just site back and relax. Watch it here:
Setting The Rules
First of all, in order to have error handling, we need to establish the rules for the form. When editing a given field in the form Blueprint, we’ll select the Validation tab, and activate the Required
rule. You can do this with the big toggle there, or with the multiselect, and choose from additional rules if you wish.
Wiring up Alpine JS
Then in our template, we’ll need to wire up some JavaScript to handle the form submission and error handling. First, we’ll change up our form
tag to look like this:
{{ form:contact attr:x-ref="form" attr:@submit.prevent="submitForm" }}
This is adding some attribues for Alpine JS attributes to hook into. This is important, so that we can actually preventDefault()
on the submit
event for this form, and let the JavaScript take over from there.
In the element containing this, we’ll also invoke the Alpine component that we will be building, which will be called contactForm
, like so:
<div x-data="contactForm">
{{ form:contact attr:x-ref="form" attr:@submit.prevent="submitForm" }}
// the rest of the form markup...
{{ /form:contact }}
</div>
Configuring our Alpine Component
In our JavaScript, we’ll set up our component to look like this:
export default () => ({
init() {
const form = this.$refs.form;
this.form = this.$refs.form;
this.formData._token = this.form.querySelector('input[name="_token"]').value;
},
errors: [],
success: false,
isSubmitting: false,
form: null,
formData: {
name: '',
email: '',
message: '',
honeypot: '',
_token: '', // Set this in init
}
})
Examining this code, we see an init
method which runs when the component is first mounted. This grabs the DOM node that matches the x-ref
from our template, which is the form element itself.
From there, we’ll grab the _token
value. This is important, since our JavaScript won’t work without this - it’s a hidden field which takes care of the CSRF stuff.
Next, we have some default variables for our inputs, and some form and error states. With these in place, we should connect the inputs, using x-model
so that Alpine can bind the value of these inputs to these variables and keep them in sync.
For example:
{{ form:contact_form }}
<input type="text" name="Name" x-model="formData.name" />
{{ /form:contact_form }}
Here, x-model="name"
corresponds to the name
key in the formData
object we created above. We’ll repeat the process for the email
and message
fields.
Now, once we have this in place, we’ll flesh out the rest of the JavaScript with our submit handler:
async submitForm(event) {
// set up the error handling
this.isSubmitting = true;
this.errors = [];
this.success = false;
try {
// get the action from the form element, and post our form to it
const response = await fetch( event.target.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify(this.formData),
});
if (response.ok) {
const result = await response.json();
if (result.success) {
this.success = true;
this.resetForm();
} else if (result.errors) {
this.errors = result.errors;
}
} else {
const errorData = await response.json();
this.errors = errorData.errors || ["Something went wrong. Please try again later."];
}
} catch (error) {
this.errors = ["An unexpected error occurred. Please try again later."];
} finally {
this.isSubmitting = false;
}
},
Show Errors with Alpine
Ok, now that we have our submit
method in place, we can update our template - we’ll replace the Antlers success/errors section with some <template>
HTML tags that Alpine will use to render our error or success messages, if we have any:
{{ form:contact_form }}
<template x-if="errors.length">
<ul class="list-disc list-inside mb-4 ">
<template x-for="error in errors">
<li x-text="error"></li>
</template>
</ul>
</template>
{{ /form:contact_form }}
If we were to inspect the Network tab when our form is submitted, if there are any errors in the response, we have them available to us in our Alpine variables. These errors occur when the Validation rules are not met when the form is submitted - and again Statamic does the heavy lifting for us to determine this.
This part of the code is where the errors are made available to is in the template; when the response result
object contains an errors key, the assignment is made:
...
} else if (result.errors) {
this.errors = result.errors;
}
The same can be applied for success
- in that case, we would have another template
tag to show if there was a success value after the form successfully submits without errors.
And just like that, we have (a rather simple) Alpine-powered AJAX form. As with most things in Statamic, they take care of the back-end, and we can handle the front-end virtually any way we like, which is super good and thoughtful of them. Thanks, Statamic guys!