As part of the project I am currently working on, I had to learn how to post files to the server using AJAX. I had never even attempted this before, so I was extra excited to learn something new. Of course, what you might learn quickly is that you cannot actually do this via "AJAX". Luckily, Rob Gonda warned me about this at the New York ColdFusion User Group when he came to talk about AJAX, and so, I went the "secret iFrame" route.
This technique sounds complicated, but it turns out that with jQuery it is actually quite easy (of course, what DOESN'T jQuery make easy, right?!?). The basic principal is that you hi-jack the form submission process and redirect it to point to a hidden iFrame that you create on the fly. This iFrame then handles the file upload the same way that any ColdFusion page would handle a file upload. Once the files are uploaded, you can either return the data, as I do, or just halt processing.
Ok, let's quickly review the code (I have to get back to work). Here is our XHTML page with the ColdFusion and AJAX demo upload form:
Launch code in new window » Download code as text file »
Very simple HTML form. What you will notice is that our FORM tag does not have any attributes. These attributes will be manipulated via jQuery once the document object model (DOM) has loaded.
Now, before we get to the cool stuff, let's take a look at our ColdFusion page that will handle the AJAX style file uploads:
Launch code in new window » Download code as text file »
This page takes any form data and searches for form fields that are in the form of "uploadX", where "X" can be a number (in between 1 and 10 in our demo). The ColdFusion template then uploads the files and keeps a running array of the server file names that get generated from the upload process. The key thing to notice is that when the files are done being uploaded, the data is returned as a HTML-wrapped JSON response. This point is very important because it is how our "AJAX" code gets data back as you would from a standard AJAX call.
Ok, now that we have our very basic XHTML file form and our ColdFusion file processing code, it's time to glue them together with a bit of jQuery magic. Here is the jQuery Javascript that hi-jacks the form upload and wires the client and server together:
Launch code in new window » Download code as text file »
There are a lot of comments in this code, which makes it seem big, but there's really only like 10 lines of functional code. I am just trying to be as clear as I can about what is going on. Basically, as I said before, we are taking over the form submission process and pointing it towards an iFrame that we are creating on the fly. Once the iFrame is done processing, it will contain the HTML/JSON response that we created via ColdFusion. This response is then extracted via the iFrame load() event listener (wired via jQuery) and evaluated (converting the JSON data into actual Javascript objects). Then, for our demo purposes, we are simply alerting the file names that we created.
Well, that's all there is to it. A little ColdFusion, a little jQuery, and suddenly, uploading files using an AJAX-like methodology is quite easy, and surprisingly fast.
Download Code Snippet ZIP File
Comments (30) | Post Comment | Ask Ben | Permalink | Other Searches | Print Page
HotFix For "The ColdFusion Mail Spool Encountered An Invalid Spool File In The Spool Directory"
June 1st 2008 - National Regular Expression Day! (Post A Comment, Win A Prize)
Thanks for posting the "secret iFrame" route it works like a charm!
Posted by Janet on May 29, 2008 at 5:17 AM
@Janet,
Glad you liked.
Posted by Ben Nadel on May 29, 2008 at 7:26 AM
Pretty nice man ... I tend to use the flash route, using swfupload (http://swfupload.org/), but I like the plain jquery solution ... Good one.
Posted by Rob Gonda on May 29, 2008 at 1:59 PM
@Rob,
I am thinking of trying the SWF upload next because I heard you can rock out a "Upload Progress" bar for that :)
Thanks again for the AJAX presentation up here in NYC. It definitely got me excited about trying to make my applications more "AJAXy" in the DHTML sense.
Posted by Ben Nadel on May 29, 2008 at 2:02 PM
I fiddled with jQuery plugin AjaxFileUpload until it worked. It doesn't work great with CFCs, but I'm able to upload and manipulate uploaded images.
http://www.phpletter.com/Our-Projects/AjaxFileUpload/
Thanks for posting an alternate method
Posted by Drew on May 29, 2008 at 7:36 PM
I was unable to get this to work. Each time it generated an error that the .js file was improperly parenthesized on line 53 - the Prompt call.
I took out the leading $( and ending ); and it worked without errors, but the file is nowhere to be found (already looked in temp).
Posted by harvey on Jun 16, 2008 at 2:56 PM
@Harvey,
Hmmm. You had the jQuery javascript file included, right?
Posted by Ben Nadel on Jun 17, 2008 at 11:52 AM
@All,
I just found a small bug in IE. Apparently, in IE, you cannot set the enctype attribute of the form. You have to set the "encoding" attribute. I am updating the code to reflect this.
Posted by Ben Nadel on Jun 25, 2008 at 12:59 PM
In Firefox 3, after the prompt appears, the browser keeps trying to connect with the site, unlike in IE where it properly just stops. So in FF3 you get a constant wait/loading mouse pointer...
Posted by Steve on Jun 27, 2008 at 1:48 PM
@Steve,
I get that occasionally, but it seems to be hit or miss. I don't think it affects the functionality at all.
Posted by Ben Nadel on Jun 27, 2008 at 2:06 PM
Ben very nice! Thank you for the tip.... ! Worked really like a charm. I also referred to Apples famous Remote Scripting Basics and came up with something similar but with the Prototype library. So if someone is looking for that.
File 1: client.php
<form id="form">
<input name="file_upload" id="file_upload" type="file" size="60" />
<span id="upload_update"></span>
</form>
<script type="text/javascript">
Event.observe(window, 'load', function(){
Event.observe('file_upload', 'change', function(){
$('form').writeAttribute({enctype: "multipart/form-data", method:"post", action:"server.php", target:"RSIFrame"});
$('form').insert(new Element("iframe", {id: "RSIFrame", name: "RSIFrame", style: "width: 0px; height: 0px; border: 0px;", src: "blank.html"}));
$('form').submit();
});
});
function showResult(result){
$('file_upload').remove();
$('upload_update').insert(new Element("a", {href: "http://localhost/upload/" + result}).update("Uploaded Image"));
}
</script>
File 2: server.php
<?php
$uploadDir = 'C:/wamp/www/upload/';
$fileName = $_FILES['file_upload']['name'];
$tmpName = $_FILES['file_upload']['tmp_name'];
$fileSize = $_FILES['file_upload']['size'];
$fileType = $_FILES['file_upload']['type'];
$filePath = $uploadDir . $fileName;
$result = move_uploaded_file($tmpName, $filePath);
?>
<script type="text/javascript">
window.parent.showResult('<?php echo $fileName;?>');
</script>
Posted by Mahesh on Jul 22, 2008 at 2:10 AM
@Mahesh,
Very cool. I have been playing around a bit with Prototype lately because of some client work and have found it to be very confusing. Granted, the client is using a slightly earlier version of prototype which means that the documentation is not as readily available.
Glad you got this up and running so easily with Prototype. In my experiments, I couldn't even get the dom:loaded event to fire :(
Posted by Ben Nadel on Jul 22, 2008 at 8:17 AM
Ben the easiest thing to get the latest version of prototype without having to bother about the version is using Google AJAX Library APIs.
Put this into any page and you have the library hooked up with the latest version. Is also available for other frameworks like jQuery and so on.
Check out: http://code.google.com/apis/ajaxlibs/documentation/
These two lines get you Prototypes latest verson 1.6
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript"> google.load("prototype", "1.6"); </script>
Thanks and looking forward to your other posts and if you have any questions on Prototype I can help. Have been using it for a long time.
Posted by Mahesh on Jul 22, 2008 at 9:55 AM
@Mahesh,
Thanks man. I have seen some buzz about the Google JS stuff, but have not looked into it just yet.
Posted by Ben Nadel on Jul 22, 2008 at 10:11 AM
For the above script to work across browsers, do not set the form enctype in the prototype writeAttribute, instead add it to the form, as IE will not do the upload, if the form is set with the encType through JS.
Posted by Mahesh on Jul 22, 2008 at 7:32 PM
@Mahesh,
Corrent; I have found that you need to set the "encoding" as well with IE.
Posted by Ben Nadel on Jul 24, 2008 at 3:16 PM
Hey there, nice code. I used it as a starting point and converted it to PHP, as well as cleaned up the jQuery a bit. The code is essentially the same however is used for image uploading.
Here's a bit cleaner jQuery:
function gogo(){
$upform = $("form");
$upform.bind("submit", function(event){
$ifrm = $("<iframe name=\"up-" + (new Date()).getTime() + "\" src=\"about:blank\" />")
.hide()
.bind("load", function(event){
$bdy = $(window.frames[$ifrm.attr("name")].document.getElementsByTagName("body")[0]);
alert($bdy.html());
$ifrm.remove();
});
$("body").append($ifrm);
nm = $ifrm.attr("name");
$upform.attr({
action: 'upload.php',
method: 'post',
enctype: 'multipart/form-data',
ecoding: 'multipart/form-data',
target: $ifrm.attr("name")
});
});
The main differences are binding instead of setting (jQuery.click is supposed to be used to trigger a click, jQuery.bind is supposed to be used to bind an action in the event of a click for example), less variables, and the proper setting of attributes. My naming convention generally follows the rule that if the variable holds a jQuery object, start it with a $. ($var as opposed to var).
I don't know if you will use it or not, but hey I thought I'd show my tweaks.
Posted by Josh on Aug 5, 2008 at 11:22 AM
Seems there is either a problem with my script and Opera 9.5 on Linux (Ubuntu 8.04), or with Opera on Linux in general. Opera does not wait until the file is loaded to continue with the script, and so there is no response before the javascript finishes loading. To solve this (which also solves ff3 constantly loading) I added in a window.setTimeout and wrapped it around the contents of the binding to the iframe. General improved syntax:
$ifrm.bind("load", function(event){
window.setTimeout(function(event){
.........
}, 500);
});
Seems less than 500 milliseconds and my short script will not have passed back. I haven't tried on a slower computer but I'd imagine a half a second would be ample time for all scripts to be run.
Posted by Josh on Aug 5, 2008 at 12:27 PM
@Josh,
I am not sure if there is any difference between using the [object].load() and the [object].bind( "load" syntax. I think one might just act as a short-hand notation for the other. I believe that .load() actually calls .bind() behind the scenes.
Thanks for the tip about the setTimeout() on the load function. I have noticed some continuous loading on FF3 (and sometimes on earlier browsers). I will try putting that into my code.
Thanks.
Posted by Ben Nadel on Aug 5, 2008 at 1:00 PM
Oh, I don't believe there is anything wrong with it. I just think it is frowned upon standards wise. It might just be me, but I reserve a direct call for when I am instigating a click or something along those lines.
On further look at the jQuery site, it would appear they don't care. Oh well, I'll continue binding anyways.
Cheers
-Josh
Posted by Josh on Aug 5, 2008 at 5:45 PM
Awesome example. Thanks Ben...this helped out a great deal.
Posted by Sam Farmer on Aug 6, 2008 at 1:00 PM
@Sam,
No problem man. I will try to implement the code that prevents it from *thinking* for ever post-form submission. If I do, I'll let you know.
Posted by Ben Nadel on Aug 7, 2008 at 1:58 PM
@Ben: I was merging this with a whole bunch of other JS and things got a little too complicated. So I added a permanent iframe to the page and always post to that (which is fine for my use case) and it solved the "always thinking" issue. Not sure what the cause was but thought that might help.
Lots of people say its good to be always thinking though...
Posted by Sam Farmer on Aug 20, 2008 at 1:57 PM
@Sam,
Hmm, maybe if I don't remove the iFrame, it will help. I don't think there is any harm to letting several IFrames build up, especially when the main page will refresh from time to time.
... it took me several reads to get the "always be thinking" joke :) It's hot in here.
Posted by Ben Nadel on Aug 21, 2008 at 2:27 PM
I updated the example code to have the a slight delay on the frame removal:
setTimeout(
. . . . function(){
. . . . . . . . jFrame.remove();
. . . . },
. . . . 100
. . . . );
This slight delay (could be less than 100, but it doesn't matter) is enough to let FireFox stop "thinking". I am not sure if this would also fix the Opera issue mentioned above.
Posted by Ben Nadel on Aug 21, 2008 at 2:46 PM
Can we send text post data with file data?
Posted by Evden Eve Nakliyat on Oct 2, 2008 at 2:57 AM
Ben, how would you integrate progress bar? Any ideas?
Steve
Posted by Steve on Oct 26, 2008 at 8:28 PM
@Steve,
In order to integrate a progress bar, I think you'd need to use something like SWFUpload. I believe that Flash-based uploading is the only way to get at that kind of information.
Posted by Ben Nadel on Oct 27, 2008 at 8:12 AM
This doesn't seem to work in Google Chrome, too bad. Any thoughts on why not? Not allowed to dynamically add iframes?
Posted by rnstr on Nov 12, 2008 at 5:04 PM
@rnstr,
I find that a lot of AJAX stuff doesn't work in Google Chrome. Not sure why. I've definitely found it to be a hit and miss browser for this sort of thing.
Posted by Ben Nadel on Nov 12, 2008 at 5:08 PM