ColdFusion, jQuery, And "AJAX" File Upload Demo

Posted May 28, 2008 at 4:05 PM

Tags: AJAX, ColdFusion, Javascript / DHTML

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 »

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>ColdFusion And AJAX File Upload Demo</title>
  •  
  • <!-- Linked scripts. -->
  • <script type="text/javascript" src="jquery-1.2.2.pack.js"></script>
  • <script type="text/javascript" src="ajax_upload.js"></script>
  • </head>
  • <body>
  •  
  • <form>
  •  
  • <h1>
  • ColdFusion And AJAX File Upload Demo
  • </h1>
  •  
  • <input type="file" name="upload1" size="60" /><br />
  • <br />
  •  
  • <input type="file" name="upload2" size="60" /><br />
  • <br />
  •  
  • <input type="submit" value="Upload Files" />
  •  
  • </form>
  •  
  • </body>
  • </html>

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 »

  • <!---
  • Create an array of files names that we are going to be
  • passing back to the client.
  • --->
  • <cfset arrFiles = [] />
  •  
  •  
  • <!---
  • Loop over form fields looking for files. We dont know if
  • or how many files are going to be uploaded at this point.
  • --->
  • <cfloop
  • index="strFileIndex"
  • from="1"
  • to="10"
  • step="1">
  •  
  • <!--- Build dynamic file field (form field key). --->
  • <cfset strField = "upload#strFileIndex#" />
  •  
  • <!---
  • Check to see if file field exists and that it has
  • value in it (file path).
  • --->
  • <cfif (
  • StructKeyExists( FORM, strField ) AND
  • Len( FORM[ strField ] )
  • )>
  •  
  • <!--- Upload file. --->
  • <cffile
  • action="upload"
  • filefield="#strField#"
  • destination="#ExpandPath( './files/' )#"
  • nameconflict="makeunique"
  • />
  •  
  • <!---
  • Add the generated server file name to the array of
  • file names that we are going to return.
  • --->
  • <cfset ArrayAppend( arrFiles, CFFILE.ServerFile ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  •  
  • <!---
  • Create the return HTML. Remember, we are going to be
  • treating the BODY of the returned document as if it
  • were a JSON string.
  • --->
  • <cfsavecontent variable="strHTML">
  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head></head>
  • <body>#SerializeJSON( arrFiles )#</body>
  • </html>
  •  
  • </cfoutput>
  • </cfsavecontent>
  •  
  •  
  • <!--- Create binary response data. --->
  • <cfset binResponse = ToBinary( ToBase64( strHTML ) ) />
  •  
  • <!--- Tell the client how much data to expect. --->
  • <cfheader
  • name="content-length"
  • value="#ArrayLen( binResponse )#"
  • />
  •  
  • <!---
  • Stream the "plain text" back to the client. It's actually
  • HTML and it is important that we announce it as HTML
  • otherwise the client might not know how to work with it.
  • --->
  • <cfcontent
  • type="text/html"
  • variable="#binResponse#"
  • />

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 »

  • // When the DOM loads, initailize the form to upload the files
  • // using an AJAX "like" call rather than a form submit.
  • $(
  • function(){
  • // Get a reference to the form we are going to be
  • // hooking into.
  • var jForm = $( "form:first" );
  •  
  • // Attach an event to the submit method. Instead of
  • // submitting the actual form to the primary page, we
  • // are going to be submitting the form to a hidden
  • // iFrame that we dynamically create.
  • jForm.submit(
  • function( objEvent ){
  • var jThis = $( this );
  •  
  • // Create a unique name for our iFrame. We can
  • // do this by using the tick count from the date.
  • var strName = ("uploader" + (new Date()).getTime());
  •  
  • // Create an iFrame with the given name that does
  • // not point to any page - we can use the address
  • // "about:blank" to get this to happen.
  • var jFrame = $( "<iframe name=\"" + strName + "\" src=\"about:blank\" />" );
  •  
  • // We now have an iFrame that is not attached to
  • // the document. Before we attach it, let's make
  • // sure it will not be seen.
  • jFrame.css( "display", "none" );
  •  
  • // Since we submitting the form to the iFrame, we
  • // will want to be able to get back data from the
  • // form submission. To do this, we will have to
  • // set up an event listener for the LOAD event
  • // of the iFrame.
  • jFrame.load(
  • function( objEvent ){
  • // Get a reference to the body tag of the
  • // loaded iFrame. We are doing to assume
  • // that this element will contain our
  • // return data in JSON format.
  • var objUploadBody = window.frames[ strName ].document.getElementsByTagName( "body" )[ 0 ];
  •  
  • // Get a jQuery object of the body so
  • // that we can have better access to it.
  • var jBody = $( objUploadBody );
  •  
  • // Assuming that our return data is in
  • // JSON format, evaluate the body html
  • // to get our return data.
  • var objData = eval( "(" + jBody.html() + ")" );
  •  
  • // "Alert" the return data (this should
  • // be an array of the server-side files
  • // that were uploaded).
  • prompt( "Return Data:", objData );
  •  
    // Remove the iFrame from the document.
  • // Because FireFox has some issues with
  • // "Infinite thinking", let's put a small
  • // delay on the frame removal.
  • setTimeout(
  • function(){
  • jFrame.remove();
  • },
  • 100
  • );
  • }
  • );
  •  
  •  
  • // Attach to body.
  • $( "body:first" ).append( jFrame );
  •  
  • // Now that our iFrame it totally in place, hook
  • // up the frame to post to the iFrame.
  • jThis
  • .attr( "action", "upload_act.cfm" )
  • .attr( "method", "post" )
  • .attr( "enctype", "multipart/form-data" )
  • .attr( "encoding", "multipart/form-data" )
  • .attr( "target", strName )
  • ;
  • }
  • );
  • }
  • );

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




Adobe ColdFusion 8.0.1 Update - Helping Programmers To Be Signifanctly Less Girlie - Download ColdFusion 8 Update 8.0.1 Now.

Reader Comments

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


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting