Skip to content

Michael LaRoy - Home

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.

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

Validation rules screenshot

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!

You're a good man

Join my Email List

Get notified about my work with Statamic - from new YouTube videos to articles and tutorials on my blog, and more.