This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.
- 31/July/2014
- Added the Android inconsistencies section
Introduction
Back in June 2010, I published a blog post detailing how to perform Ajax file uploads from your HTML forms. The result worked pretty well, but there was still some room for browser vendors to make things even simpler for us.
Turns out, they have. The specific improvement that made this possible is the
FormData
interface, first introduced in Safari 5, and later in
Chrome 7 and Firefox 4.
Compatibility and detection
As usual, this doesn’t really work across all browsers in the market (you know
the usual suspects). Also as usual, the best way to know if the client supports
this feature is object detection. I have updated the function we used in 2010,
adding a check for the FormData
interface:
function supportAjaxUploadWithProgress() {
return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData();
function supportFileAPI() {
var fi = document.createElement('INPUT');
fi.type = 'file';
return 'files' in fi;
};
function supportAjaxUploadProgressEvents() {
var xhr = new XMLHttpRequest();
return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
};
function supportFormData() {
return !! window.FormData;
}
}
If the function returns true, you are good to go. If not, it may still be that the old technique can still be used (although chances are slim). If neither is, you will have to resort to some other way to do this, such as Flash (argh!) or traditional form submission.
Submitting the whole form
I won’t dwell in details such as the events, as they haven’t changed since the
last time. What has changed is what the
XMLHttpRequest.send()
call receives. Last year, we fed it an instance of the
File
interface. This time instead, we will give it an instance of FormData
.
FormData
gives us two ways to interface with it. The first and simplest is:
get a reference to the form
element and pass it to the FormData
constructor,
like so:
var form = document.getElementById('form-id');
var formData = new FormData(form);
This new FormData
instance is all you need to pass on the
send()
call:
var xhr = new XMLHttpRequest();
// Add any event handlers here...
xhr.open('POST', '/upload/path', true);
xhr.send(formData);
This will send an Ajax request with all the fields of the form on it, not only
file inputs. If there were also text areas, text fields, checkboxes or what have
you, they’ll be sent too. Any events that you may be listening to will be
called, such as onprogress
or onreadystatechange
.
Submitting only the file
The solution proposed above, albeit useful, is a bit limiting in that we are forced to submit the whole form. Instead, we may want to submit the file independently of the rest of the form. This is common nowadays in places such as bulk photo uploaders on many social networks.
Fortunately FormData
also allows us to do it this way. For this, it provides
the method append(name, value)
:
<!-- The HTML -->
<input id="the-file" name="file" type="file">
// The Javascript
var fileInput = document.getElementById('the-file');
var file = fileInput.files[0];
var formData = new FormData();
formData.append('file', file);
And now the formData
object is ready to be sent using XMLHttpRequest.send()
as in the previous example.
Progressive enhancement
One immediate advantage over the old method: the data received on the server side will be indistinguishable from a normal form submission. With the old method, we had to add specific code on the server side to handle the incoming data in a specialised way. This is not necessary anymore, and removed code is debugged code!
There is another great advantage: it is easier to add progressive enhancement. We can have a perfectly normal form, and add this Ajax on top. This way, if the necessary JS interfaces aren’t supported, the form will still work (although not as nicely). It would be something like this:
<!-- The HTML -->
<form id="the-form" action="/upload/path" enctype="multipart/form-data">
<input name="file" type="file">
<input type="submit" value="Upload" />
</form>
// The Javascript
var form = document.getElementById('the-form');
form.onsubmit = function() {
var formData = new FormData(form);
formData.append('file', file);
var xhr = new XMLHttpRequest();
// Add any event handlers here...
xhr.open('POST', form.getAttribute('action'), true);
xhr.send(formData);
return false; // To avoid actual submission of the form
}
In the example above, we send the XMLHttpRequest on submit of the form. We also
read the action
attribute of the form to know where to send the request. Of
course, it’s missing some event handlers that we’ll use to update the interface.
The beauty of this example is: if the object detection script returns false, this form will still work, and therefore functionality will remain intact.
Android inconsistencies
But there’s still one problem: some Android devices don’t play entirely well with this technique. They sort of work, but the load and progress events won’t fire. I have found this problem across versions of the OS and browser apps. After all, remember that there’s no such thing as The Android Browser.
This is not the end of the world though. A way to mitigate this problem is to make our webapp work so that it doesn’t really matter much if those two events are not fired. Obvious when you think of it:
- Listen to the loadstart event and show some kind of pseudo-progress feedback. For example, an indeterminate progress bar or a spinner.
- If and when progress is triggered, remove this pseudo-progress element and replace it with a real progress bar.
- Don’t rely on the load event to know when the upload has finished. Instead, use good old readystatechange. However, bear in mind there’s a subtle difference in behaviour: load will trigger when the upload is finished, without waiting for the response from the server. On the other hand readystatechange will wait for a response from the server, with occurs later.
A complete example
But enough of theory. I have updated the old example to use this new technique. You can find this updated example at GitHub. There are comments at every step of the client code, but don’t hesitate to ask if anything is not clear.