Ajax upload with XMLHttpRequest level 2 and the File API
Posted on November 17, 2010 by Phil 30 comments
I’ve put together a micro Ajax library for the XMLHttpRequest level 2 spec. You can go check it out on Github or read the related article XMLHttpRequest Level 2 Ajax library, xhr2-lib
Ajax applications are wide spread these days and one thing that has proven a pain in the arse is uploading files. I’m sure most people will have fired off a fake asynchronous upload to a hidden iframe or used a sneaky swf to handle it but now with the development of the level 2 specification for the XMLHttpRequest object some of those woes may soon (if you are ok about not supporting IE) be a thing of the past.
The FileReader API and the DataTransfer object
One of the coolest things I’ve messed about with so far in HTML5 is the FileReader and File APIs and their ability to read in files from the users computer. With this also comes the addition of the DataTransfer object which carries a reference to the files in the drag/drop events. What this means is that we can drag files from our desktop and drop them onto our web applications where we can use JavaScript to read in their data or pass them off to be uploaded. The code snippets below are taken from the FileAPI singleton used in the demo to handle the file manipulation and uploading.
this.showDroppedFiles = function (ev) {
ev.stopPropagation();
ev.preventDefault();
var files = ev.dataTransfer.files;
addFileListItems(files);
}
The above method is fired when the user drops files onto a specified element, we stop any default action for the event and stop it bubbling before accessing the dataTransfer object of the drop event to retrieve the list of files that were dropped before passing them to the addFileListItems method shown below.
var addFileListItems = function (files) {
for (var i = 0; i < files.length; i++) {
var fr = new FileReader();
fr.file = files[i];
fr.addEventListener("loadend", showFileInList, false);
fr.readAsDataURL(files[i]);
}
}
Here we loop over our file list and for each one we initialise a FileReader object and bind the showFileInList method to it’s loadend event which fires once our FileReader object is done reading in the file as a data URL. Note: my testing revealed that addEventListener is not supported for FileReader in Chrome. Currently Safari and IE9 do not support the file API. It is only necessary to do this to show the preview for images in the list (see demo) other files could just be added to the queue directly. In the showFileInList method (shown below) we access the file we have just read, populate a list with it’s details and push it into an Array ready to be uploaded. Also you’ll notice the addition of an upload progress indicator which we’ll look at with the uploading via Ajax.
var showFileInList = function (ev) {
var file = ev.target.file;
if (file) {
var li = document.createElement("li");
if (file.type.search(/image\/.*/) != -1) {
var thumb = new Image();
thumb.src = ev.target.result;
thumb.addEventListener("mouseover", showImagePreview, false);
thumb.addEventListener("mouseout", removePreview, false);
li.appendChild(thumb);
}
var h3 = document.createElement("h3");
var h3Text = document.createTextNode(file.name);
h3.appendChild(h3Text);
li.appendChild(h3)
var p = document.createElement("p");
var pText = document.createTextNode(
"File type: ("
+ file.type + ") - " +
Math.round(file.size / 1024) + "KB"
);
p.appendChild(pText);
li.appendChild(p);
var divLoader = document.createElement("div");
divLoader.className = "loadingIndicator";
li.appendChild(divLoader);
fileList.appendChild(li);
fileQueue.push({
file : file,
li : li
});
}
}
XMLHttpRequest object send() with File
In the level 2 specification the send method can accept a File object argument which allows us to stream binary chunked data to the server asynchronously. So when we have chosen the files we want to upload and have our queue all set up ready to roll we can fire each file off to our upload method to be sent to the server for saving as shown in the two methods below.
this.uploadQueue = function (ev) {
ev.preventDefault();
while (fileQueue.length > 0) {
var item = fileQueue.pop();
var p = document.createElement("p");
p.className = "loader";
var pText = document.createTextNode("Uploading...");
p.appendChild(pText);
item.li.appendChild(p);
if (item.file.size < 1048576) {
uploadFile(item.file, item.li);
} else {
p.textContent = "File to large";
p.style["color"] = "red";
}
}
}
In this method we loop through our file list and send each one in turn, I only have the file size check at 1MB for the demo – can be higher. I set up some loading text here to let the user know what’s going on, we also have a progress bar which was set up earlier that we will update as the file uploads (explained below).
var uploadFile = function (file, li) {
if (li && file) {
var xhr = new XMLHttpRequest(),
upload = xhr.upload;
upload.addEventListener("progress", function (ev) {
if (ev.lengthComputable) {
var loader = li.getElementsByTagName("div")[0];
loader.style["width"] = (ev.loaded / ev.total) * 100 + "%";
}
}, false);
upload.addEventListener("load", function (ev) {
var ps = li.getElementsByTagName("p");
var div = li.getElementsByTagName("div")[0];
div.style["width"] = "100%";
div.style["backgroundColor"] = "#0f0";
for (var i = 0; i < ps.length; i++) {
if (ps[i].className == "loader") {
ps[i].textContent = "Upload complete";
ps[i].style["color"] = "#3DD13F";
break;
}
}
}, false);
upload.addEventListener("error", function (ev) {console.log(ev);}, false);
xhr.open(
"POST",
"upload.php"
);
xhr.setRequestHeader("Cache-Control", "no-cache");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.setRequestHeader("X-File-Name", file.name);
xhr.send(file);
}
}
With the level 2 implementation of XMLHttpRequest we have access to it’s associated XMLHttpRequestUpload object with it’s very handy progress event handler with which we can create progress indicators. The progress event handler presents the lengthComputable attribute which if true enables us to calculate the progress by taking what is loaded against the total. All we need do is set the relevant http headers, including a custom header which we need to use to pass our file name to the server to save the file, and call the send method with the file object as the argument.
Summary
Although the code is somewhat experimental I think it is something that I will most definitely use in an application in the not too distant future, Firefox and Chrome are at the time of writing the only browsers with support for FileReader and File APIs and I should imagine Safari is not far behind. As for Internet Explorer 9…. who knows?
References
Related Posts
No related posts.
30 Responses So Far
-
-
Bob Robinson January 19, 2011 at 3:24 pm
Brilliant demo. This is the best one I have seen for using both the FileAPI and XMLHttpRequest Level 2 together for an upload with progress, client side. Thanks for posting this!
-
Graphics and Web Design Firm April 6, 2011 at 9:56 am
Wow! nice demo very helpful and informative thanks for sharing such a good work.
-
Shane May 24, 2011 at 4:19 am
Thanks for the demo. However, for some reason, the files don’t always upload. I’m not sure why this is, but maybe you have an explanation. It seems to have an upload progress for each file, but on my server, it doesn’t show all files as being uploaded.
-
pierlo July 30, 2011 at 12:38 pm
Great tutorial. One question though, why did you choose not to implement this using jQuery? At least when it comes to DOM manipulation like this
var ps = li.getElementsByTagName("p");
var div = li.getElementsByTagName("div")[0];
div.style["width"] = "100%";
div.style["backgroundColor"] = "#0f0";
etc…(just for my curiosity)
-
Pingback: Removing Flash from OS X: My experience, frustrations & some HTML5 alternatives » Joe Lambert
-
farouknl October 2, 2011 at 11:25 pm
A best script. thank you.
I want to use it in php and I want this form to send a text ID to the database.
Example
form action=”" method=”post” enctype=”multipart/form-data”>
input type=”file” id=”fileField” name=”fileField” multiple />
input type=\”hidden\” name=\”text_id\” value=\”$text_id\”>I am new to JavaScript and do not know how to do this.
Who will help me?thanks
-
Kenaniah November 3, 2011 at 11:23 pm
Instead of loading the image inline as base64 content, try using the createObjectURL() method by changing line 92 of FileAPI.js to:
thumb.src = window.URL ? window.URL.createObjectURL(file) : window.webkitURL.createObjectURL(file);
See https://developer.mozilla.org/en/DOM/window.URL.createObjectURL for more info
-
gerteb December 18, 2011 at 8:06 pm
Great tutorial and sample code. Is there any limitations on usage of the code. Wold like to integrate it with modifications in a prorietary project, i a have been working on for some years now.
-
gerteb December 19, 2011 at 11:29 am
After testing the script in FF and Chrome i discovered that dropping large amount of 3-5Mb pictures made the browsers crash! Opera 11.6 doesn’t accept dropping files.
The reason i believe is memory caching of “readAsDataURL”, and too many working threads. Rewrote the function addFileListItems listed below, so it is recursive instead of itearative. Now only one picture is delt with at file read from filesystem, and the image object is cleared from memory. The function showImagePreview has to be rewritten to read the image file or omitted.
At test after this rewrite FF stops after 34 file reads, and Chrome stops after 334 file reads. That is why there is a second condition i the for statment in the first line of the function.
Hope it is of any use.
var addFileListItems = function (files,i) {
if(i<files.length&&i<25){
var file = files[i];
if (file) {
var li = document.createElement("li");
if (file.type.search(/image\/.*/) != -1) {
window.URL=window.webkitURL || window.URL; // Vendor prefixed in Chrome.
var thumb = document.createElement('img');
thumb.onload = function(e) {
window.URL.revokeObjectURL(thumb.src); // Clean up after yourself.
i++;
addFileListItems(files,i);// Recursive call
};
thumb.src = window.URL.createObjectURL(file);
thumb.addEventListener("mouseover", showImagePreview, false);
thumb.addEventListener("mouseout", removePreview, false);
li.appendChild(thumb);
}
var h3 = document.createElement("h3");
var h3Text = document.createTextNode(file.name);
h3.appendChild(h3Text);
li.appendChild(h3)
var p = document.createElement("p");
var pText = document.createTextNode("File type: ("+file.name+")-"+Math.round(file.size / 1024) + "KB");
p.appendChild(pText);
li.appendChild(p);
var divLoader = document.createElement("div");
divLoader.className = "loadingIndicator";
li.appendChild(divLoader);
fileList.appendChild(li);
fileQueue.push({
file : file,
li : li
});
}
}
}
-
Pingback: Gock's Blog » 略谈浏览器Upload File (上传文件)
-
Matteo January 11, 2012 at 11:33 am
Hi,
Great tutorial. I would ask you if can i check the height and width of the file before to upload it. -
Pingback: XMLHttpRequest Level 2 Ajax library, xhr2-lib | Phil Parsons
-
Craig February 20, 2012 at 10:58 pm
Great write up! I’ve been playing with this myself lately and can’t wait for IE to support it (v10!).
One thing that I can’t find is the ability to iterate through a formData object to remove a file(s) a user may have accidentally uploaded. Say they drag a group of 5 files on to a dropzone, and then decide they want to remove one, before submitting. I can’t see a way to do that, short of clearing the whole object – any ideas?





nice demo. I’ve worked with the ZIP; but I’m missing the “Streamer.php” file that implements the File_Streamer class. with php://input i’ve reconstructed some of that but I’m wondering if I’m missing out on something. Is that class avaliable somewhere?