Sending forms through JavaScript

As shown in a previous article, an HTML form is a convenient way to set up an HTTP request in a declarative way. But in many cases, forms can also be useful for setting up an HTTP request using just JavaScript code. There are several ways to handle this; this article will explore them.

A form is not always a form

With the advent of open Web apps, it's increasingly common to use HTML forms in ways other than as literal forms for humans to fill out; more and more developers are trying to take control over the process of transmitting data.

Gaining control of the global interface

The main reason to control the way data gets sent is to remain in control of the user interface (UI). Standard use of HTML forms requires loading the page to which the data is sent, which means the entire page is refreshed. In many modern interfaces, that's usually something we want to avoid in order to give a smooth experience to the user by removing flickering or by hiding network lag.

To reach that goal, many modern UIs use HTML forms only to collect data from the user. Once the user is ready to send the data, the application takes control and sends the data asynchronously in the background, dealing with the UI to change only the parts that require changes.

Sending arbitrary data asynchronously is known as AJAX, which is an acronym that stands for "Asynchronous JavaScript And XML."

How is it different?

AJAX techniques mainly rely on the XMLHttpRequest (XHR) DOM object. That object is a powerful tool that lets you build HTTP requests, send them, then retrieve the result of those requests.

Note: There are other AJAX techniques that do not rely on the XMLHttpRequest DOM object. For example, a mix of JSONP and the use of the eval() function can be used. While that works, it's not recommended because it can lead to serious security issues. The only reason to do that is to deal with the lack of support for XMLHttpRequest or JSON in legacy browsers, but those are very, very old browsers indeed! You should avoid such techniques.

From a historical point of view, XMLHttpRequest was intended to be used with XML as an exchange format. However, due to its overwhelming adoption rate, JSON has superseded XML. That said, there's no right or wrong choice here: JSON is a lightweight, structured format, while XML is a more semantic format. On the one hand, JSON is a better choice if you need to reduce the network footprint; on the other hand, XML offers more information about the data itself and its structure. The choice is up to you.

Still, in both cases, such structured data doesn't fit the structure of form data requests. In their most basic form, form data requests are URL-encoded lists of key/value pairs. For binary data, the whole HTTP request is reshaped to handle it.

If you control both the front-end (that is, the code that's executed in the browser) and the back-end (which is executed on the web server), this isn't a problem, because you can define the data structure on both sides as you see fit.

Unfortunately, if you want to use a third party service, it's not that easy. Sometimes, you have to deal with services that accept data as a form data request only. There are also cases where it's just simpler to deal with form data. If the data consists of key/value pairs, or if you want to send binary data, there are many stacks of back-end tools already available to handle data of that nature.

So how is it possible to send such data?

Sending form data

There are actually three ways to send form data, starting with legacy techniques to the much newer FormData object. Let's look at these techniques in more detail.

Building a DOM in a hidden iframe

The oldest way to send form data programmatically is by building a form using the DOM API, then sending its data into a hidden <iframe>. You just have to retrieve the content of the hidden <iframe> if you want to access the result of what you sent.

Warning: This technique has many drawbacks and you should avoid using it. It's a potential security risk when used with third-party services because it's an open door to script injection attacks. If you use HTTPS, it can have effects on the same origin policy, which can make the content of an <iframe> unreachable. However, this method will work with very old browsers, and may be your only option if you need to support archaic software.

Here is a simple example:

<button onclick="sendData({test:'ok'})">Click Me!</button>

All the magic is in the script

// Let's create the iFrame used to send our data
var iframe = document.createElement("iframe");
iframe.name = "myTarget";

// Next, attach the iFrame to the main document
window.addEventListener("load", function () {
  iframe.style.display = "none";
  document.body.appendChild(iframe);
});

// This is the function used to actually send the data
// It takes one parameter, which is an object populated with key/value pairs.
function sendData(data) {
  var name,
      form = document.createElement("form"),
      node = document.createElement("input");

  // Define what should happen when the response is loaded
  iframe.addEventListener("load", function () {
    alert("Yeah! Data sent.");
  });
    
  form.action = "http://www.cs.tut.fi/cgi-bin/run/~jkorpela/echo.cgi";
  form.target = iframe.name;

  for(name in data) {
    node.name  = name;
    node.value = data[name].toString();
    form.appendChild(node.cloneNode());
  }

  // To be sent, the form needs to be attached to the main document.
  form.style.display = "none";
  document.body.appendChild(form);

  form.submit();

  // But once the form is sent, it's useless to keep it.
  document.body.removeChild(form);
}

And here's the live result:

Building an XMLHttpRequest manually

XMLHttpRequest is easily the safest and most reliable way to handle HTTP requests. To send form data with XMLHttpRequest, you need to handle encoding the data properly; all data must be URL-encoded. You also need to know the specifics of form data requests.

Note: If you want to know more about using XMLHttpRequest, there are two articles that may interest you: An introductory article to AJAX and a more advanced article about using XMLHttpRequest itself.

Let's rebuild our previous example:

<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>

As you can see, the HTML hasn't really changed. However, the underlying JavaScript code is completely different:

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var urlEncodedData = "";
  var urlEncodedDataPairs = [];
  var name;

  // We turn the data object into an array of URL encoded key value pairs.
  for(name in data) {
    urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
  }

  // We combine the pairs into a single string and replace all encoded spaces to 
  // the plus character to match the behaviour of the web browser form submit.
  urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+');

  // We define what will happen if the data is successfully sent
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // We define what will happen in case of error
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // We setup our request
  XHR.open('POST', 'http://ucommbieber.unl.edu/CORS/cors.php');

  // We add the required HTTP header to handle a form data POST request
  XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  XHR.setRequestHeader('Content-Length', urlEncodedData.length);

  // And finally, We send our data.
  XHR.send(urlEncodedData);
}

And here's the live result:

Note: This kind of use of XMLHttpRequest is subject to the same origin policy if you want to send data to a third party web site. To perform cross origin requests, you should familiarize yourself with CORS and HTTP access control.

Using XMLHttpRequest and the FormData object

Building an HTTP request by hand is always a bit overwhelming. Fortunately, a recent evolution of the XMLHttpRequest specification provides a convenient and simpler way to handle form data requests. It's the FormData object.

The FormData object can be used in two ways; you can use it to build a set of data to transmit, or to get the data represented by a given form element so you can manage how it gets sent. It's worth noting that FormData objects are "write only" objects, which means you can make changes to them, but you can't retrieve their contents.

How to use this type of object is detailed in the article Using FormData Objects, but here are two simple examples:

Using a standalone FormData object

<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>

You should be familiar with that HTML sample.

function sendData(data) {
  var XHR = new XMLHttpRequest();
  var FD  = new FormData();

  // We push our data into our FormData object
  for(name in data) {
    FD.append(name, data[name]);
  }

  // We define what will happen if the data are successfully sent
  XHR.addEventListener('load', function(event) {
    alert('Yeah! Data sent and response loaded.');
  });

  // We define what will happen in case of error
  XHR.addEventListener('error', function(event) {
    alert('Oups! Something goes wrong.');
  });

  // We setup our request
  XHR.open('POST', 'http://ucommbieber.unl.edu/CORS/cors.php');

  // We just send our FormData object, HTTP headers are set automatically
  XHR.send(FD);
}

And here's the live result:

Using FormData bound to a form element

You can also bind a FormData object to a <form> element. This lets you quickly get a FormData that represents the data contained in the form.

The HTML part looks pretty typical:

<form id="myForm">
  <label for="myName">Send me your name:</label>
  <input id="myName" name="name" value="John">
  <input type="submit" value="Send Me!">
</form>

But JavaScript takes over the form.

window.addEventListener("load", function () {
  function sendData() {
    var XHR = new XMLHttpRequest();

    // We bind the FormData object and the form element
    var FD  = new FormData(form);

    // We define what will happen if the data are successfully sent
    XHR.addEventListener("load", function(event) {
      alert(event.target.responseText);
    });

    // We define what will happen in case of error
    XHR.addEventListener("error", function(event) {
      alert('Oups! Something goes wrong.');
    });

    // We setup our request
    XHR.open("POST", "http://ucommbieber.unl.edu/CORS/cors.php");

    // The data sent are the one the user provide in the form
    XHR.send(FD);
  }
 
  // We need to access the form element
  var form = document.getElementById("myForm");

  // to takeover its submit event.
  form.addEventListener("submit", function (event) {
    event.preventDefault();

    sendData();
  });
});

And here's the live result:

Dealing with binary data

One last word about binary data. If you use a FormData object bound to a form that includes file widgets, the data will be processed automatically, but if you want to send binary data by hand, there's some extra work to do.

There are many possible sources for binary data in modern Web design: the FileReader API, the Canvas API, and the WebRTC API are common ones. Unfortunately, some legacy browsers are not able to access binary data or require complicated workarounds. Accessing binary data in those legacy cases is out of the scope of this article. If you want to know more about the FileReader API, you can read Using files from web applications.

However, sending binary data with a browser that supports FormData isn't hard at all. Just use the append() method as usual and you're done. If, however, you have to do it by hand, it's trickier.

In the following example, we use the FileReader API to access the binary data and we build the multi-part form data request by hand:

<form id="myForm">
  <p>
    <label for="i1">text data:</label>
    <input id="i1" name="myText" value="Some text data">
  </p>
  <p>
    <label for="i2">file data:</label>
    <input id="i2" name="myFile" type="file">
  </p>
  <button>Send Me!</button>
</form>

As you see, the HTML is a very standard form. There's nothing magical going on. The "magic" is in the JavaScript code:

// Because we want to access DOM node,
// we initialize our script at page load.
window.addEventListener('load', function () {

  // This variables will be used to store the form data
  var text = document.getElementById("i1");;
  var file = {
        dom    : document.getElementById("i2"),
        binary : null
      };
 
  // We use the FileReader API to access our file content
  var reader = new FileReader();

  // Because de FileReader API is asynchronous, we need
  // to store it's result when it has finish to read the file
  reader.addEventListener("load", function () {
    file.binary = reader.result;
  });

  // At page load, if a file is already selected, we read it.
  if(file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // However, we will read the file once the user selected it.
  file.dom.addEventListener("change", function () {
    if(reader.readyState === FileReader.LOADING) {
      reader.abort();
    }
    
    reader.readAsBinaryString(file.dom.files[0]);
  });

  // The sendData function is our main function
  function sendData() {
    // At first, if there is a file selected, we have to wait it is read
    // If it is not, we delay the execution of the function
    if(!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // To construct our multipart form data request,
    // We need an XMLHttpRequest instance
    var XHR      = new XMLHttpRequest();

    // We need a sperator to define each part of the request
    var boundary = "blob";

    // And we'll store our body request as a string.
    var data     = "";

    // So, if the user has selected a file
    if (file.dom.files[0]) {
      // We start a new part in our body's request
      data += "--" + boundary + "\r\n";

      // We said it's form data (it could be something else)
      data += 'content-disposition: form-data; '
      // We define the name of the form data
            + 'name="'         + file.dom.name          + '"; '
      // We provide the real name of the file
            + 'filename="'     + file.dom.files[0].name + '"\r\n';
      // We provide the mime type of the file
      data += 'Content-Type: ' + file.dom.files[0].type + '\r\n';

      // There is always a blank line between the meta-data and the data
      data += '\r\n';
      
      // We happen the binary data to our body's request
      data += file.binary + '\r\n';
    }

    // For text data, it's simpler
    // We start a new part in our body's request
    data += "--" + boundary + "\r\n";

    // We said it's form data and give it a name
    data += 'content-disposition: form-data; name="' + text.name + '"\r\n';
    // There is always a blank line between the meta-data and the data
    data += '\r\n';

    // We happen the text data to our body's request
    data += text.value + "\r\n";

    // Once we are done, we "close" the body's request
    data += "--" + boundary + "--";

    // We define what will happen if the data are successfully sent
    XHR.addEventListener('load', function(event) {
      alert('Yeah! Data sent and response loaded.');
    });

    // We define what will happen in case of error
    XHR.addEventListener('error', function(event) {
      alert('Oups! Something goes wrong.');
    });

    // We setup our request
    XHR.open('POST', 'http://ucommbieber.unl.edu/CORS/cors.php');

    // We add the required HTTP header to handle a multipart form data POST request
    XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary);
    XHR.setRequestHeader('Content-Length', data.length);

    // And finally, We send our data.
    // Due to Firefox's bug 416178, it's required to use sendAsBinary() instead of send()
    XHR.sendAsBinary(data);
  }

  // At least, We need to access our form
  var form   = document.getElementById("myForm");

  // to take over the submit event
  form.addEventListener('submit', function (event) {
    event.preventDefault();
    sendData();
  });
});

And here's the live result:

Note: The non-standard sendAsBinary method in the example above is considered deprecated as of Gecko 31 (Firefox 31 / Thunderbird 31 / SeaMonkey 2.28) and will be removed soon. The standard send(Blob data) method can be used instead.

Conclusion

Depending on the browser you want to target, sending form data through JavaScript can be easy or really difficult; if you're only concerned about modern browsers, it can be quite simple. If you need to support legacy browsers, however, it gets more complicated. The FormData object is generally the answer to all your troubles, and you shouldn't hesitate to use a polyfill for it on legacy browsers:

Document Tags and Contributors

 Last updated by: uddin,