Freeform Documentation

Learn the basics, and get to know Freeform inside and out.

Freeform 1.x Docs

Submitting a form using AJAX

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).

NOTE: AJAX currently will not work with multi-page forms.

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>

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"]
}

Usage in Templates #

NOTE: 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>

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>

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>

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