Submitting a form using AJAX
To submit a form using AJAX - pass the serialized form data as the payload when posting to any front-end URL.
This solution currently will not work with multi-page forms.
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 oftruefinished- A boolean value oftruereturnUrl- The return URL specified for the formsubmissionId- Anintvalue of the submission ID if one was generated
On form error
success- A boolean value offalsefinished- A boolean value offalseerrors- An object of field handles as keys and each containing an array of error messages.- An example, if the form's 
firstNameandlastNamefields were required, but not filled out, the returning object would be: 
- An example, if the form's 
 
"errors": {
	"firstName": ["This field is required"],
	"lastName": ["This field is required"]
}
Usage in Templates
Here's a fully working Bootstrap form AJAX example:
<script>
  var form = document.getElementById('my-form');
  form.addEventListener('submit', function (event) {
    let data = new FormData(form);
    let request = new XMLHttpRequest();
    // Safari hack - remove empty file upload inputs
    // Otherwise an ajax call with empty file uploads causes immense lag
    if (navigator.userAgent.indexOf('Safari') > -1) {
      for (let 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');
    // Set the AJAX headers
    request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    request.setRequestHeader('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest');
    request.onload = () => {
      if (request.status === 200) {
        var response = JSON.parse(request.response);
        var success = response.success;
        var finished = response.finished;
        var actions = response.actions ? response.actions : [];
        var errors = response.errors;
        var formErrors = response.formErrors;
        var honeypot = response.honeypot;
        if (!actions.length) {
          if (success && finished) {
            // Reset the form so that the user may enter fresh information
            form.reset();
            // ==================================
            // Perform something after the
            // form saves successfully
            // ==================================
          } else if (errors || formErrors) {
            // ==================================
            // Do something with the errors here
            // ==================================
            console.error(errors, formErrors);
          }
        }
        // ==================================
        // Honeypot update logic
        // ==================================
        if (honeypot) {
          var honeypotInput = form.querySelector(
            'input[name^=freeform_form_handle]'
          );
          if (honeypotInput) {
            honeypotInput.setAttribute('name', honeypot.name);
            honeypotInput.setAttribute('id', honeypot.name);
            honeypotInput.value = honeypot.hash;
          }
        }
      } else {
        console.error(request);
      }
    };
    request.send(data);
    event.stopPropagation();
    event.preventDefault();
    return false;
  });
</script>