24

I'm working with a form for which the mark-up I can't change & can't use jQuery. Currently the form post the results to a new window. Is it possible to change this to an ajax form so that the results displays on submit instead without altering any mark-up? Pulling the results (mark-up) from the results page back to the form page.

Here is the mark-up for the form.

<form class="form-poll" id="poll-1225962377536" action="/cs/Satellite" target="_blank">
<div class="form-item">
    <fieldset class="form-radio-group">
        <legend><span class="legend-text">What mobile phone is the best?</span></legend>
                <div class="form-radio-item">
                    <input type="radio" class="radio" value="1225962377541" name="option" id="form-item-1225962377541">
                    <label class="radio" for="form-item-1225962377541">
                        <span class="label-text">iPhone</span>
                    </label>
                </div><!-- // .form-radio-item -->
                <div class="form-radio-item">
                    <input type="radio" class="radio" value="1225962377542" name="option" id="form-item-1225962377542">
                    <label class="radio" for="form-item-1225962377542">
                        <span class="label-text">Android</span>
                    </label>
                </div><!-- // .form-radio-item -->
                <div class="form-radio-item">
                    <input type="radio" class="radio" value="1225962377543" name="option" id="form-item-1225962377543">
                    <label class="radio" for="form-item-1225962377543">
                        <span class="label-text">Symbian</span>
                    </label>
                </div><!-- // .form-radio-item -->
                <div class="form-radio-item">
                    <input type="radio" class="radio" value="1225962377544" name="option" id="form-item-1225962377544">
                    <label class="radio" for="form-item-1225962377544">
                        <span class="label-text">Other</span>
                    </label>
                </div><!-- // .form-radio-item -->
    </fieldset>
</div><!-- // .form-item -->
<div class="form-item form-item-submit">
    <button class="button-submit" type="submit"><span>Vote now</span></button>
</div><!-- // .form-item -->
<input type="hidden" name="c" value="News_Poll">
<input type="hidden" class="pollId" name="cid" value="1225962377536">
<input type="hidden" name="pagename" value="Foundation/News_Poll/saveResult">
<input type="hidden" name="site" value="themouth">

Any tips/tutorial is much appreciated. :)

calebo
  • 3,312
  • 10
  • 44
  • 66

9 Answers9

21

The following is a far more elegant solution of the other answer, more fit for modern browsers.

My reasoning is that if you need support for older browser you already most likely use a library like jQuery, and thus making this question pointless.

/**
 * Takes a form node and sends it over AJAX.
 * @param {HTMLFormElement} form - Form node to send
 * @param {function} callback - Function to handle onload. 
 *                              this variable will be bound correctly.
 */

function ajaxPost (form, callback) {
    var url = form.action,
        xhr = new XMLHttpRequest();

    //This is a bit tricky, [].fn.call(form.elements, ...) allows us to call .fn
    //on the form's elements, even though it's not an array. Effectively
    //Filtering all of the fields on the form
    var params = [].filter.call(form.elements, function(el) {
        //Allow only elements that don't have the 'checked' property
        //Or those who have it, and it's checked for them.
        return typeof(el.checked) === 'undefined' || el.checked;
        //Practically, filter out checkboxes/radios which aren't checekd.
    })
    .filter(function(el) { return !!el.name; }) //Nameless elements die.
    .filter(function(el) { return el.disabled; }) //Disabled elements die.
    .map(function(el) {
        //Map each field into a name=value string, make sure to properly escape!
        return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
    }).join('&'); //Then join all the strings by &

    xhr.open("POST", url);
    // Changed from application/x-form-urlencoded to application/x-form-urlencoded
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

    //.bind ensures that this inside of the function is the XHR object.
    xhr.onload = callback.bind(xhr); 

    //All preperations are clear, send the request!
    xhr.send(params);
}

The above is supported in all major browsers, and IE9 and above.

adonald
  • 3
  • 1
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
  • 7
    Some people can't use jQuery for performance reasons, the question isn't pointless. – JmJ Oct 09 '15 at 12:57
  • That should be "application/x-www-form-urlencoded", not "application/x-form-urlencoded". Found this problem because my servers CORS implementation rejected it based on this header. Additionally, params seems to be coming up blank for some reason, but I haven't debugged that. – cgag Jan 04 '16 at 07:31
  • @cgag feel free to edit, or wait for me to do it later today – Madara's Ghost Jan 04 '16 at 07:39
  • 2
    Shouldn't it be `!el.disabled`? – Roy J Jul 05 '16 at 17:35
  • Yes, There is a typo in this. It should be !el.disabled. This code wasn't passing on the csrfmiddlewaretoken in my form either.. Needs tweaks. – user2283043 May 03 '19 at 11:22
14

Here's a nifty function I use to do exactly what you're trying to do:

HTML:

<form action="/cs/Satellite">...</form>
<input type="button" value="Vote now" onclick="javascript:AJAXPost(this)">

JS:

function AJAXPost(myself) {
    var elem   = myself.form.elements;
    var url    = myself.form.action;    
    var params = "";
    var value;

    for (var i = 0; i < elem.length; i++) {
        if (elem[i].tagName == "SELECT") {
            value = elem[i].options[elem[i].selectedIndex].value;
        } else {
            value = elem[i].value;                
        }
        params += elem[i].name + "=" + encodeURIComponent(value) + "&";
    }

    if (window.XMLHttpRequest) {
        // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp=new XMLHttpRequest();
    } else { 
        // code for IE6, IE5
        xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
    }

    xmlhttp.open("POST",url,false);
    xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xmlhttp.setRequestHeader("Content-length", params.length);
    xmlhttp.setRequestHeader("Connection", "close");
    xmlhttp.send(params);

    return xmlhttp.responseText;
}
Coomie
  • 4,832
  • 3
  • 32
  • 44
  • 1
    `.elements` returns only input nodes for `FORM`? – Tomáš Zato Sep 09 '13 at 20:06
  • Yes the .elements of a form object return only input nodes – Alain Beauvois Sep 12 '13 at 20:06
  • 1
    This solution will send unchecked radio buttons and checkboxes. – Madara's Ghost Oct 24 '14 at 20:44
  • 1
    @AlainBeauvois— *element* returns **all** controls in the form, not just inputs. – RobG Oct 25 '14 at 21:04
  • @Coomie sorry I can't make it work. Can you expand the form example? Are they valid `this` in the onClick and the `myself` in the function? Sorry I'm not strong with js. `this` looks referred to the button itself – Robert Feb 22 '19 at 22:11
  • @Robert Try putting `alert(params);` after the for loop. That will tell you what your params are. `this` is the button, but `myself.form` is the form, `myself.form.elements` is all the elements of the form for the button. elem may include all HTML tags, so you might want to filter to inputs. – Coomie Feb 26 '19 at 07:43
8

Nowadays using FormData is the easiest method. You construct it with a reference to the Form element, and it serializes everything for you.

MDN has an example of this here -- roughly:

const form = document.querySelector("#debarcode-form");
form.addEventListener("submit", e => {
    e.preventDefault();
    const fd = new FormData(form);
    const xhr = new XMLHttpRequest();
    xhr.addEventListener("load", e => {
        console.log(e.target.responseText);
    });
    xhr.addEventListener("error", e => {
        console.log(e);
    });
    xhr.open("POST", form.action);
    xhr.send(fd);
});

and if you want it as an object (JSON):

const obj = {};
[...fd.entries()].forEach(entry => obj[entry[0]] = entry[1]);
ZachB
  • 13,051
  • 4
  • 61
  • 89
3

Expanding on Madara's answer: I had to make some changes to make it work on Chrome 47.0.2526.80 (not tested on anything else). Hopefully this can save someone some time.

This snippet is a modification of that answer with the following changes:

  • filter !el.disabled,
  • check type of input before excluding !checked
  • Request type to x-www-form-urlencoded

With the following result:

function ajaxSubmit(form, callback) {
    var xhr = new XMLHttpRequest();
    var params = [].filter.call(form.elements, function (el) {return !(el.type in ['checkbox', 'radio']) || el.checked;})
    .filter(function(el) { return !!el.name; }) //Nameless elements die.
    .filter(function(el) { return !el.disabled; }) //Disabled elements die.
    .map(function(el) {
        return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
    }).join('&'); //Then join all the strings by &
    xhr.open("POST", form.action);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.onload = callback.bind(xhr);
    xhr.send(params);
};
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
Vince
  • 645
  • 4
  • 10
  • Thanks, this worked. I found the x-www problem but not the other two. There's a strange "·" character at the end of this line by the way: "xhr.onload = callback.bind(xhr);·". – cgag Jan 04 '16 at 07:39
  • I love this answer. My only problem with it is that it does not get if a check mark is checked or not and no doubt it will have the same problem with radio. See my answer below for edited version of yours that solves the check mark part of it. – Brandan Apr 17 '16 at 09:04
2

A modern way using fetch would be:

const formData = new FormData(form);
fetch(form.action, {
  method: 'POST',
  body: formData
});

Note browser support and use this polyfil if IE-support is needed

ferdynator
  • 6,245
  • 3
  • 27
  • 56
  • Are you sure that you don't need the content-type that other suggest? That's what I'm doing in my [fetch-based npm module](https://github.com/fregante/push-form/blob/master/index.ts) – fregante Nov 22 '20 at 23:18
  • Probably depends on the server side parsing. I cannot find a default value definition for content-type in the draft. So it is probably better to explicitly set it. – ferdynator Nov 23 '20 at 14:24
2

The strategy is to serialise the form and send the data using XHR, then do what you want with the response. There is a good set of utilities and help at Matt Krus's Ajax Toolbox and related Javascript Toolbox.

If you are just serialising the form posted, then the following will do the trick. It can easily be extended to include other form control types:

var serialiseForm = (function() {

  // Checkboxes that have already been dealt with
  var cbNames;

  // Return the value of a checkbox group if any are checked
  // Otherwise return empty string
  function getCheckboxValue(cb) {
    var buttons = cb.form[cb.name];
    if (buttons.length) {
      for (var i=0, iLen=buttons.length; i<iLen; i++) {
        if (buttons[i].checked) {
          return buttons[i].value;
        }
      }
    } else {
      if (buttons.checked) {
        return buttons.value;
      }
    }
    return '';
  }

  return function (form) {
    var element, elements = form.elements;
    var result = [];
    var type;
    var value = '';
    cbNames = {};

    for (var i=0, iLen=elements.length; i<iLen; i++) {
      element = elements[i];
      type = element.type;

      // Only named, enabled controls are successful
      // Only get radio buttons once
      if (element.name && !element.disabled && !(element.name in cbNames)) {

         if (type == 'text' || type == 'hidden') {
          value = element.value;

        } else if (type == 'radio') {
          cbNames[element.name] = element.name;
          value = getCheckboxValue(element);

        }
      }

      if (value) {
        result.push(element.name + '=' + encodeURIComponent(value));
      }
      value = '';

    }
    return '?' + result.join('&');
  }
}());
RobG
  • 142,382
  • 31
  • 172
  • 209
0
function ajaxSubmit(form, callback) {
var xhr = new XMLHttpRequest();
var params = [].filter.call(form.elements, function (el) {return !(el.type in ['checkbox', 'radio']) || el.checked;})
.filter(function(el) { return !!el.name; }) //Nameless elements die.
.filter(function(el) { return !el.disabled; }) //Disabled elements die.
.map(function(el) {
    if (el.type=='checkbox') return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.checked);
   else return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value);
}).join('&'); //Then join all the strings by &
xhr.open("POST", form.action);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onload = callback.bind(xhr);
xhr.send(params);

};

Brandan
  • 307
  • 1
  • 4
  • 12
0

I just took Coomie's answer above and made it work for Radio/Checkboxes. I can't believe how simple and clear this is. With a few exceptions, I'm done using frameworks.

    var params = "";
    var form_elements = form.elements;
    for (var i = 0; i < form_elements.length; i++) 
    {
        switch(form_elements[i].type)
        {
            case "select-one":
            {
                value = form_elements[i].options[form_elements[i].selectedIndex].value;
            }break;
            case "checkbox":
            case "radio": 
            {
                if (!form_elements[i].checked)
                {
                    continue; // we don't want unchecked data
                }
                value = form_elements[i].value;
            }break;
            case "text" :
            {
                value = form_elements[i].value;                
            }break;

        }

        params += encodeURIComponent(form_elements[i].name) + "=" + encodeURIComponent(value) + "&";
    }



    var xhr = new XMLHttpRequest();    
    xhr.open('POST', "/api/some_url");
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200)
            {

                console.log("xhr.responseText");
            }
            else
            {
                console.log("Error! Status: ", xhr.status, "Text:", xhr.responseText);
            } 
        }
    };

    console.log(params);
    xhr.send(params);
Seth
  • 134
  • 1
  • 14
-1

Here's the simplest method I came up with. I haven't found an example that uses this exact approach. The code submits the form using a non-submit type button and places the results into a div, if the form is not valid (not all required fields filled), it will ignore the submit action and the browser itself will show which fields are not filled correctly.

This code only works on modern browsers supporting the "FormData" object.

<script>
function ajaxSubmitForm() {
  const form = document.getElementById( "MyForm" );
  if (form.reportValidity()) {
    const FD = new FormData( form );
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("content_area").innerHTML = this.responseText; } };
    xhttp.open("POST","https://example.com/whatever.php",true);
    xhttp.send( FD );
  }
}
</script>

<div id="content_area">
<form id="MyForm">
  <input type="hidden" name="Token" Value="abcdefg">
  <input type="text" name="UserName" Value="John Smith" required>
  <input type="file" accept="image/jpeg" id="image_uploads" name="ImageUpload" required>
  <button type="button" onclick="ajaxSubmitForm()">
</form>
</div>
bLight
  • 803
  • 7
  • 23