Almost a year ago I wrote a blog post discussing how to use the Cordova FileTransfer plugin to upload multiple files (Processing multiple simultaneous uploads with Cordova). In that post I demonstrated how you could wrap the calls to each upload in a Promise and then wait for them all to complete. A reader pointed out the obvious issue with that solution - for N files you're creating N HTTP requests. While probably not a big deal (and as the developer, you could put a limit on how many files were allowed to be sent), I thought it would be interesting to demonstrate how can upload multiple files with one POST request using XHR2.
First off - a quick refresher. What is XHR2? Easy - the second version of XHR. Ok, sorry, that was a bit sarcastic. XHR is the abbreviation of XMLHttpRequest and is how JavaScript performs Ajax requests. Basically, XHR is what you think of as Ajax. If you've only ever done Ajax via jQuery, then just know that $.get is wrapping calls to an XHR object behind the scenes. Working with XHR without jQuery isn't terribly difficult - you can start with this great guide at MDN for a refresher.
XHR2 represents the more recent version of the overall spec (details here) and includes the ability to send file uploads via Ajax. Previously this was done with products like Adobe Flash. Support for XHR2 is rather nice:
And of course, if you want to use this in Cordova, you are definitely safe to do so, especially if you use Crosswalk on the Android side.
So, how do you send a file using XHR2? For that I'd suggest reading the excellent MDN article, Using FormData Objects, which walks you through the process of creating a FormData object in JavaScript and assigning a file to it. I won't repreat the documentation there as it is short and sweet (and once again I will remind folks that the Mozilla Developer Network is one of the best damn resources on the Internet for web developers) but rather focus on how we could use it to send multiple files at once.
Let's begin with an incredibly simple handler for our uploads. The following Node application defines a route, /upload
, that uses the Formidable package to process file uploads. It literally just dumps them to console so it really isn't "handling" it, but it gave me an easy way to see my form POSTs come in and ensure they were being sent the right way.
var express = require('express');
var app = express();
var formidable = require('formidable');
app.set('port', process.env.PORT || 3000);
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.post('/upload', function(req, res) {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
console.log('handling form upload - fields', fields);
console.log('handling form upload - files', files);
});
res.send('Thank you');
});
app.listen(app.get('port'), function() {
console.log('Express running on http://localhost:' + app.get('port'));
});
Ok, now let's look at the front end. For my first demo, I created the following HTML.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>
<form id="testForm">
<input type="file" id="file1"><br/>
<input type="file" id="file2"><br/>
<input type="file" id="file3"><br/>
<input type="submit">
</form>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="app.js"></script>
</body>
</html>
As you can see - I've got 3 form fields - all of which are file types. Now let's look at the code.
var $f1, $f2, $f3;
$(document).ready(function() {
$('#testForm').on('submit', processForm);
$f1 = $('#file1');
$f2 = $('#file2');
$f3 = $('#file3');
});
function processForm(e) {
e.preventDefault();
console.log('processForm');
var formData = new FormData();
if($f1.val()) formData.append('file1', $f1.get(0).files[0]);
if($f2.val()) formData.append('file2', $f2.get(0).files[0]);
if($f3.val()) formData.append('file3', $f3.get(0).files[0]);
var request = new XMLHttpRequest();
request.open('POST', 'http://localhost:3000/upload');
request.send(formData);
request.onload = function(e) {
console.log('Request Status', request.status);
};
}
So first off - yes I'm using jQuery and naked XHR calls. That's ok. You don't have to use jQuery and that's fine too. Don't stress over it. I begin by adding a submit handler and a quick reference to my three fields. When you submit the form, I create a new FormData
object. The MDN article I linked to earlier talks about it more, but you can think of it like a virtual form. I can add simple name/value pairs (like name = 'Raymond') as well as files. I check the value of each field to determine if something was selected. Here's an important point though. The value will be a string. In Chrome it looks something like C:\fakepath\aliens.jpg
. Yes, I'm on a Mac and yes, that's a fake Windows path. The idea here is to obscure the real path so that nasty code can't be used as a vector to attack your system.
The FormData object though wants a real file. We use $f1.get(0)
to connect to the real DOM item (not the jQuery-wrapped object) and then use the files
property to gain read access to the selected file.
That's repeated for each of the three fields and then I simply post to my server.
And yeah - that's it. When I test, I can see my files come in as expected.
So far so good. But don't forget that some HTML form tags support a multiple
attribute. In v2 of my sample, I changed the three form fields to this:
&;lt;input type="file" id="file1" multiple>
I can now select 0-N files when submitting my form. Here is the modified version of the code to handle that.
var $f1;
$(document).ready(function() {
$('#testForm').on('submit', processForm);
$f1 = $('#file1');
});
function processForm(e) {
e.preventDefault();
console.log('processForm');
var formData = new FormData();
if($f1.val()) {
var fileList = $f1.get(0).files;
for(var x=0;x<fileList.length;x++) {
formData.append('file'+x, fileList.item(x));
console.log('appended a file');
}
}
var request = new XMLHttpRequest();
request.open('POST', 'http://localhost:3000/upload');
request.send(formData);
request.onload = function(e) {
console.log('Request Status', request.status);
};
}
I grab that files
property and treat it like a simple array. It isn't quite an array though (MDN docs for FileList) as I have to use a item
method to fetch the particular value. Then it's simply a matter of appending to formData
with a unique name for each file. This is treated the exact same by the back-end so it just works.
All in all - pretty simple I think and could be used easily in a Cordova application. You can also attach a progress event handler and monitor the process of sending up the binary data. Enjoy and let me know if you have any questions by leaving a comment below.