Skip to main content

AJAX Forms with Freeform

Welcome to the guide for submitting your forms using AJAX. The good news is that we've made it easy to plug and play!

You're viewing the v3.9 version of these docs. To switch to the v3.10 docs, click here to view the revised v3.10 documentation that shows usage of AJAX and the improved Freeform Javascript plugin.

Overview

Freeform has its own built-in automated AJAX support. To enable it, simply check the Enable AJAX checkbox in the Form Settings tab inside the Form Builder and Freeform will then handle the rest. Freeform includes its own javascript plugin which handles AJAX functionality and all other JS needed (such as Date fields, Stripe payments and more). Much of the default functionality (formatting, language, etc) can be overridden if necessary. See customization reference guide below.

If you need to use a manual approach for the AJAX call, usually because some sort of framework is used to generate the form such as Vue.js, please see the Manual Implementations guide further below.

Be sure to check out the Freeform Javascript plugin documentation, to learn how to further extend Freeform's functionality.

AJAX currently will not work with multi-page forms.

Placement of Script Overrides

The placement of your JS overrides will depend on what your Freeform Javascript Insertion Location setting is set to. This is general information that will apply to all scenarios further below (light customization to advanced options). See those options for more complete solutions.

When using Inside Form option, the Freeform JS plugin instance will be accessible to scripts that are called inside the footer. Load your custom script in the footer and call the Freeform JS plugin instance directly from the form object, like so:

const form = document.getElementById('my-form');
form.freeform.setOption(
'successBannerMessage',
'This is a custom success message.'
);

If you have set the Freeform Javascript Insertion Location setting set to Page Footer, then it's best to attach an event listener to the form object which will execute once the Freeform JS plugin instance has finished initializing. Use this script before the rest of the footer scripts, but after the form has been rendered, like so:

const form = document.getElementById('my-form');
form.addEventListener('freeform-ready', function (event) {
event.target.freeform.setOption(
'successBannerMessage',
'This is a custom success message.'
);
});

Multiple Forms in the Same Page

If you have multiple forms in the same page, and wish to have a single set of JS overrides, you can loop through all forms like this:

If Javascript Insertion Location setting is set to Inside Form:

<script>
const forms = document.querySelectorAll('form[data-id]');
for (const form of forms) {
const freeform = form.freeform;

freeform.setOption('successBannerMessage', "This is a custom success message.");
}
</script>
<script>
const forms = document.querySelectorAll('form[data-id]');
for (const form of forms) {
form.addEventListener("freeform-ready", function (event) {
const freeform = event.target.freeform;
freeform.setOption('successBannerMessage', "This is a custom success message");
});
}
</script>

If loading the same form more than once in the same template, be sure to specify the id parameter for each instance so that AJAX submit can account for this.

Alternate Success Markup

If you wish to override the default AJAX success message and instead load alternate success markup from a template or existing div, check out our guide for this:

Display Success Banner at Bottom

If you wish to display the success banner at the bottom of the form, here's an example of how to do so. The success banner will have the class ff-form-success added to it, which will allow it to be automatically removed again on consequent saves.

{{ craft.freeform.form("myForm").render }}

<script>
var form = document.querySelector('form');
form.addEventListener('freeform-render-success', function (event) {
event.preventDefault();

var freeform = event.freeform;
var success = document.createElement('div');
success.classList.add(freeform.options.successClassBanner);
success.appendChild(document.createTextNode('Form submitted successfully'));

form.appendChild(success);
});
</script>

Customization

The built-in AJAX functionality lets you completely customize the way your forms work if you're not satisfied with anything in the provided defaults. If you only need to customize the error messages, you can do so by overriding the defaults like this:

// Find the form element
const form = document.getElementById('my-form');

// Attach an on-ready listener
// Be sure to do load this script before freeform JS initiates
form.addEventListener('freeform-ready', function (event) {
const freeform = event.target.freeform;

// Customize the error and success messages
freeform.setOption('successBannerMessage', 'My custom success message');
freeform.setOption('errorBannerMessage', 'My custom error message');
});

A more elaborate example below shows how you can customize the way error and success messages are rendered:

// Find the form element
const form = document.getElementById('my-form');

// Attach on-ready listener
// Be sure to do load this script before freeform JS initiates
form.addEventListener('freeform-ready', function (event) {
const freeform = event.target.freeform;

// Override the error and success message element class names
freeform.setOption('errorClassBanner', 'my-custom-error-banner');
freeform.setOption('errorClassList', 'my-custom-errors-list');
freeform.setOption('errorClassField', 'this-field-has-errors');
freeform.setOption('successClassBanner', 'my-custom-success-banner');

// Override the way those messages are removed
freeform.setOption('removeMessages', function () {
this.form.querySelectorAll('.my-custom-error-banner').remove();
this.form.querySelectorAll('.my-custom-success-banner').remove();
this.form.querySelectorAll('.my-custom-errors-list').remove();
});

// Override the way form submit success message is displayed
freeform.setOption('renderSuccess', function () {
const successMessage = document.createElement('div');
successMessage.classList.add('my-custom-success-banner');
successMessage.appendChild(
document.createTextNode('Form submitted successfully')
);

this.form.insertBefore(successMessage, this.form.childNodes[0]);
});

// Override the way form errors are displayed
freeform.setOption('renderFormErrors', function (errors) {
const errorBlock = document.createElement('div');
errorBlock.classList.add('my-custom-errors-banner');

const paragraph = document.createElement('p');
paragraph.appendChild(document.createTextNode('Form contains errors!'));
errorBlock.appendChild(paragraph);

if (errors.length) {
const errorsList = document.createElement('ul');
for (let messageIndex = 0; messageIndex < errors.length; messageIndex++) {
const message = errors[messageIndex];
const listItem = document.createElement('li');

listItem.appendChild(document.createTextNode(message));
errorsList.appendChild(listItem);
}

errorBlock.appendChild(errorsList);
}

this.form.insertBefore(errorBlock, this.form.childNodes[0]);
});

// Override the way field errors are displayed
freeform.setOption('renderFieldErrors', function (errors) {
for (const key in errors) {
if (!errors.hasOwnProperty(key) || !key) {
continue;
}

const messages = errors[key];
const errorsList = document.createElement('ul');
errorsList.classList.add('my-custom-errors-list');

for (
let messageIndex = 0;
messageIndex < messages.length;
messageIndex++
) {
const message = messages[messageIndex];
const listItem = document.createElement('li');
listItem.appendChild(document.createTextNode(message));
errorsList.appendChild(listItem);
}

const inputList = this.form.querySelectorAll(
'*[name=' + key + "], *[name='" + key + "[]']"
);
for (let inputIndex = 0; inputIndex < inputList.length; inputIndex++) {
const input = inputList[inputIndex];
input.classList.add('this-field-has-errors');
input.parentElement.appendChild(errorsList);
}
}
});
});

Advanced

To attach your own functionality when the AJAX form successfully submits or fails, you have to listen to the respective events dispatched by the Freeform JS plugin. For instance, redirecting after an AJAX submit is finished:

// Find the form element
const form = document.getElementById('my-form');

// Attach on-ready listener
// Be sure to do load this script before freeform JS initiates
form.addEventListener('freeform-ready', function (event) {
const freeform = event.target.freeform;

// Do something on a successful ajax submit
freeform.addOnSuccessfulAjaxSubmit(function (event, form, response) {
// Redirect the user
window.location.href = response.returnUrl;
});

// Do something on a failed ajax submit
freeform.addOnFailedAjaxSubmit(function (event, form, response) {
// Do whatever checks on errors you need to do, if any
var errors = response.errors;

// Redirect the page using the response's return URL
window.location.href = response.returnUrl;
});

// Return the form to the specified Return URL upon success
freeform.addOnAfterAjaxSubmit((event, form, response) => {
if (response.returnUrl) {
window.location.href = response.returnUrl;
}
});

// Do something after every ajax submit is completed, regardless of it being successful or not
freeform.addOnAfterAjaxSubmit(function (event, form, response) {
// Redirect the page using the response's return URL
window.location.href = response.returnUrl;
});
});

Manual Implementations

There are cases when an AJAX call has to be made manually. Usually it is because some sort of framework is used to generate the form, such as Vue.js. All you need to do when making a manual AJAX request is pass the AJAX specific headers within the request:

{
"X-Requested-With": "XMLHttpRequest"
"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"
}

By default, Freeform will assume you're using its own built-in AJAX. To prevent Freeform from interfering with your custom AJAX implementation, you should also add a data-skip-html-reload form attribute to your form.

Here are some examples:

Plain JS

To create an AJAX request using regular plain javascript, follow this example:

// Find the form element
var form = document.getElementById('my-form');

// Add the on-submit event listener
form.addEventListener('submit', ajaxifyForm, false);

function ajaxifyForm(event) {
var form = event.target;
var data = new FormData(form);

var method = form.getAttribute('method');
var action = form.getAttribute('action');

var request = new XMLHttpRequest();
request.open(method, action ? action : window.location.href, true);
request.setRequestHeader('Cache-Control', 'no-cache');

// Set the Craft specific AJAX headers
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.setRequestHeader('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest');

request.onload = function () {
if (request.status === 200) {
var response = JSON.parse(request.response);

if (response.success && response.finished) {
form.reset();
alert('Form posted successfully');
} else if (response.errors || response.formErrors) {
console.log('Field Errors', response.errors);
console.log('Form Errors', response.formErrors);

alert('Form failed to submit');
}

// Update the Honeypot field if using JS enhancement
if (response.honeypot) {
var honeypotInput = form.querySelector(
'input[name^=freeform_form_handle_]'
);
honeypotInput.setAttribute('name', response.honeypot.name);
honeypotInput.setAttribute('id', response.honeypot.name);
honeypotInput.value = response.honeypot.hash;
}
} else {
console.error(request);
}
};

request.send(data);
event.preventDefault();
}

Axios

To create an AJAX request using the Axios library, follow this example:

// Find the form element
var form = document.getElementById('my-form');

// Add the on-submit event listener
form.addEventListener('submit', ajaxifyForm, false);

function ajaxifyForm(event) {
var form = event.target;
var data = new FormData(form);

var method = form.getAttribute('method');
var action = form.getAttribute('action');

axios({
url: action ? action : window.location.href,
method: method ? method : 'post',
data: data,
headers: {
'X-Requested-With': 'XMLHttpRequest',
HTTP_X_REQUESTED_WITH: 'XMLHttpRequest',
},
})
.then(function (responseObject) {
var response = responseObject.data;

if (response.success && response.finished) {
form.reset();
alert('Form posted successfully');
} else if (response.errors || response.formErrors) {
console.log('Field Errors', response.errors);
console.log('Form Errors', response.formErrors);

alert('Form failed to submit');
}

// Update the Honeypot field if using Javascript Test
if (response.honeypot) {
var honeypotInput = form.querySelector(
'input[name^=freeform_form_handle_]'
);
honeypotInput.setAttribute('name', response.honeypot.name);
honeypotInput.setAttribute('id', response.honeypot.name);
honeypotInput.value = response.honeypot.hash;
}
})
.catch(function (error) {
console.error(error);
});

event.preventDefault();
}