Submitting a form using AJAX

WARNING

AJAX currently will not work with multi-page forms.

Freeform now has built-in automated AJAX submit support. To enable, simply check the Enable AJAX checkbox in the Form Settings tab inside Composer. Freeform will then handle the rest (as long as you're using a standard formatting template - very custom formatting templates may not work properly).

If you wish to customize the success and error messages for the form, you'd have to edit (or add extra override JS to your page) in the template/formatting template to control the message and styling. You can additionally add the translate |t filter to it to allow static translations in your Freeform translation file to handle other site languages. This additional code isn't necessary at all if you only want to translate the success and error strings and you're using a formatting template that already contains the following JS override code. In Freeform they default to: Form has been submitted successfully! and Error! Please review the form and try submitting again.. Also see add/control redirect for AJAX-enabled forms.

<script>
    window.renderFormSuccess = function (form) {
        const successMessage = document.createElement("div");
        successMessage.classList.add("alert", "alert-success", "form-success");

        const paragraph = document.createElement("p");
        paragraph.classList.add("lead");
        paragraph.appendChild(document.createTextNode("{{ "Form has been submitted successfully!"|t('freeform') }}"));

        successMessage.appendChild(paragraph);

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

    window.renderFormErrors = (errors, form) => {
      const errorBlock = document.createElement("div");
      errorBlock.classList.add("alert", "alert-danger", "errors");

      const paragraph = document.createElement("p");
      paragraph.appendChild(document.createTextNode("{{ "Error! Please review the form and try submitting again."|t('freeform') }}"));
      paragraph.classList.add("lead");
      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);
      }

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

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

If you do wish to use something less automated, check out the AJAX examples in the Freeform Demo Templates or check out the documentation below...

To submit a form using AJAX - pass the serialized form data as the payload when posting to any front-end URL.

Return values

The AJAX request must be a post request and it will return a JSON object with the following values:

On successful single-page form post

  • success - A boolean value of true
  • finished - A boolean value of true
  • returnUrl - The return URL specified for the form
  • submissionId - An int value of the submission ID if one was generated
  • honeypot - A JS object containing these values:
    • name - the generated input name of the honeypot field
    • hash - the generated hash value that has to be submitted for the honeypot to validate

On form error

  • success - A boolean value of false
  • finished - A boolean value of false
  • formErrors - An array of translatable form error messages
  • honeypot - A JS object containing these values:
    • name - the generated input name of the honeypot field
    • hash - the generated hash value that has to be submitted for the honeypot to validate
  • errors - An object of field handles as keys and each containing an array of error messages.
    • An example, if the form's firstName and lastName fields were required, but not filled out, the returning object would be:
"errors": {
	"firstName": ["This field is required"],
	"lastName": ["This field is required"]
}
1
2
3
4

Usage in Templates

TIP

To see a variety of complete working AJAX examples for the most popular frameworks, install and check out our Demo Templates included with Freeform. The JS used in the demo templates work in the latest browsers with plain vanilla javascript and do not require any JS libraries, like jQuery, etc.

Here's an example of the JS code that turns any form into an AJAX-ready form. It strives to cover most use-cases and allows for easy customization:

<script>
  var chrome = navigator.userAgent.indexOf("Chrome") > -1;
  var explorer = navigator.userAgent.indexOf("MSIE") > -1;
  var firefox = navigator.userAgent.indexOf("Firefox") > -1;
  var safari = navigator.userAgent.indexOf("Safari") > -1;
  var camino = navigator.userAgent.indexOf("Camino") > -1;
  var opera = navigator.userAgent.toLowerCase().indexOf("op") > -1;

  if (window.renderFormSuccess === undefined) {
    function renderFormSuccess(form) {
      var successMessage = document.createElement("div");
      successMessage.classList.add("alert", "alert-success", "form-success");

      var paragraph = document.createElement("p");
      paragraph.classList.add("lead");
      paragraph.appendChild(document.createTextNode("Form has been submitted successfully!"));

      successMessage.appendChild(paragraph);

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

  if (window.removeMessages === undefined) {
    function removeMessages(form) {
      // Remove any existing errors that are being shown
      form.querySelectorAll("ul.errors").remove();
      var fieldsWithErrors = form.querySelectorAll(".has-error");
      for (var fieldIndex = 0; fieldIndex < fieldsWithErrors.length; fieldIndex++) {
        var field = fieldsWithErrors[fieldIndex];
        field.classList.remove("has-error");
      }

      // Remove success messages
      form.querySelectorAll(".form-success").remove();
      document.getElementsByClassName("form-errors").remove();
    }
  }

  if (window.renderErrors === undefined) {
    /**
     * @param errors
     * @param form
     */
    function renderErrors(errors, form) {
      for (var key in errors) {
        if (!errors.hasOwnProperty(key) || !key) {
          continue;
        }

        var messages = errors[key];
        var errorsList = document.createElement("ul");
        errorsList.classList.add("errors", "help-block");

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

        var inputList = form.querySelectorAll("*[name=" + key + "], *[name='" + key + "[]']");
        for (var inputIndex = 0; inputIndex < inputList.length; inputIndex++) {
          var input = inputList[inputIndex];

          input.parentElement.classList.add("has-error");
          input.parentElement.appendChild(errorsList);
        }
      }
    }
  }

  if (window.renderFormErrors === undefined) {
    function renderFormErrors(errors, form) {
      var errorBlock = document.createElement("div");
      errorBlock.classList.add("alert", "alert-danger", "form-errors");

      var paragraph = document.createElement("p");
      paragraph.classList.add("lead");
      paragraph.appendChild(document.createTextNode("Error! Please review the form and try submitting again."));
      errorBlock.appendChild(paragraph);

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

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

        errorBlock.appendChild(errorsList);
      }

      form.insertBefore(errorBlock, form.childNodes[0]);
    }
  }
</script>

<script>

  function lookForFormsToAjaxify() {
    var forms = document.getElementsByTagName("form");

    for (var formIndex = 0; formIndex < forms.length; formIndex++) {
      var form = forms[formIndex];

      if (!form.dataset.ajaxified) {
        form.dataset.ajaxified = true;
        form.addEventListener("submit", ajaxifyForm, false);
      }
    }
  }

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

    if (safari) {
      for (var i = 0; i < form.elements.length; i++) {
        if (form.elements[i].type == "file") {
          if (form.elements[i].value == "") {
            var elem = form.elements[i];
            data.delete(elem.name);
          }
        }
      }
    }

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

    request.open(method, action ? action : window.location.href, true);
    request.setRequestHeader("Cache-Control", "no-cache");
    request.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    request.setRequestHeader("HTTP_X_REQUESTED_WITH", "XMLHttpRequest");
    request.onload = function () {
      removeMessages(form);

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

        if (response.success && response.finished) {
          // Reset the form so that the user may enter fresh information
          form.reset();

          // ============================================================
          // Uncomment this to have the form redirect to the success page
          // ============================================================
          // if (response.returnUrl) {
          //   window.location.href = response.returnUrl;
          // }

          renderFormSuccess(form);

        } else if (response.errors || response.formErrors) {
          renderErrors(response.errors, form);
          renderFormErrors(response.formErrors, form);
        }

        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;
        }

        unlockSubmit(form);
      } else {
        console.error(request);
      }

      unlockSubmit(form);
    };

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

  function loadExternalForm(url, targetElement) {
    var request = new XMLHttpRequest();

    // Load the forms content into the #form-loader div
    request.open("GET", url, true);
    request.send();
    request.onload = function () {
      if (request.status === 200) {
        targetElement.innerHTML = request.response;

        // Activate all of the loaded scripts
        var scripts = targetElement.querySelectorAll('script');
        for (var index = 0; index < scripts.length; index++) {
          var script = scripts[index];
          var newScript = document.createElement('script');
          newScript.innerHTML = script.innerHTML;
          targetElement.appendChild(newScript);

          script.parentNode.removeChild(script);
        }

        lookForFormsToAjaxify();
      } else {
        console.error(request);
      }
    };
  }

  /**
   * Remove the "disabled" state of the submit button upon successful submit
   *
   * @property form
   */
  function unlockSubmit(form) {
    form.querySelector("button[type=submit]").removeAttribute("disabled");
    if (typeof grecaptcha !== "undefined") {
      grecaptcha.reset();
    }
  }

  // Add remove prototypes
  Element.prototype.remove = function () {
    this.parentElement.removeChild(this);
  };

  NodeList.prototype.remove = HTMLCollection.prototype.remove = function () {
    for (var i = this.length - 1; i >= 0; i--) {
      if (this[i] && this[i].parentElement) {
        this[i].parentElement.removeChild(this[i]);
      }
    }
  };

  lookForFormsToAjaxify();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

Caching and Refreshing CSRF Token

If you're caching a form and need to manually refresh the CSRF token, you may need something like this:

<div id="target"></div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
  $(() => {
    $.ajax({
      url: "/ajax",
      success: (response) => {
        $("#target").html(response);
        $('input[name={{ craft.app.config.general.csrfTokenName }}]').val('{{ craft.app.request.csrfToken }}');
      }
    });
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

reCAPTCHA and Loading Entire Form via AJAX

If you're loading an entire form via AJAX and using reCAPTCHA, you'll need to load the reCAPTCHA JS yourself, since it's considered insecure otherwise and the browser blocks it. You should add this script tag anywhere on your page, preferably the footer:

<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?render=explicit"></script>
1

Redirect AJAX-enabled Forms

If you want to redirect an AJAX-enabled form after a successful or unsuccessful submit, you would need to attach event listeners to the form HTML element:

// A successful form submit will emit a "form.submit.success" event
form.addEventListener("form.submit.success", (event) => {
  var formElement = event.form;
  var response = event.response;

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

// An unsuccessful form submit will emit a "form.submit.error" event
form.addEventListener("form.submit.error", (event) => {
  var formElement = event.form;
  var response = event.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;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Last Updated: 1/15/2019, 12:09:09 AM