jQuery Plugin for Requesting Ajax-like File Downloads

Posted by Scott on 03/02/2009

Topics:

Ajax has changed the way we build web apps, allowing rich communication between the client and server without any need to refresh the page. But despite its power and flexibility, Ajax has numerous shortcomings such as a same-domain request policy and the inability to receive data without polling the server. For these limitations, we've seen workarounds such as JSONP and Comet.

One issue we have not yet seen addressed is the Ajax’s inability to receive a response in any form but text. Since it is now common for web applications to offer options for exporting your data in desktop app formats — such as .doc or .xls — we wrote a jQuery plugin to facilitate requests from the front end that result in a file for download. The plugin does not actually use Ajax, but its syntax follows the conventions of jQuery's native Ajax functions, making it a natural addition to our jQuery toolset.

The Problem

Let's take the example of a productivity web app such as a spreadsheet editor, which has the ability to open, save, import and export. The open and save options would involve loading a spreadsheet from the database, whereas import and export deal with local files on the user's machine. To implement the export behavior, you might decide that the user should have to save their spreadsheet first, allowing you to export the data from the backend to file. But let's assume instead you'd like to allow users to export their data without saving, perhaps to afford them the option of working locally without ever storing data on the server. In order to do this, you'd need to send the current spreadsheet data to the backend and receive a file to download. Unfortunately, this can not be handled using Ajax, since Ajax can only receive responses in the form of text. In cases where the data to be saved is rather lengthy, this poses a considerable problem.

The Workaround

In order to make the request, you'd need to make a regular (not Ajax) HTTP request using GET or POST. If the data is reasonably short, you might get away with a GET request (perhaps by simply setting Window.location to your export url), but due to varying browser limitations on GET request length, a POST will most likely be needed. The following plugin allows you to make a request that returns a file in a similar syntax to jQuery's native Ajax functions.

The Source Code


jQuery.download = function(url, data, method){
	//url and data options required
	if( url && data ){ 
		//data can be string of parameters or array/object
		data = typeof data == 'string' ? data : jQuery.param(data);
		//split params into form inputs
		var inputs = '';
		jQuery.each(data.split('&'), function(){ 
			var pair = this.split('=');
			inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />'; 
		});
		//send request
		jQuery('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>')
		.appendTo('body').submit().remove();
	};
};

How to Use It

Usage is simple. The plugin accepts 3 arguments for url, data, and method. You can pass data to these arguments just as you would to jQuery's $.post or $.get functions, and assuming the server has no problems handling the request, the front end will respond with a prompt for a file download and the user never needs to leave the page. Here's an example call to the plugin:


$.download('/export.php','filename=mySpreadsheet&format=xls&content=' + spreadsheetData );

As you can see, we've directed the request to the export.php file via the url argument, and our data argument is a key/value query string. Just like jQuery's Ajax functions, the data argument accepts either query parameters or a Javascript array or object. The method argument represents the HTTP method being used, and it defaults to 'post'. You can override this argument with 'get' depending on what your server is set up to receive.

Grab the Plugin

You can copy the source to this plugin above or download it here: jQuery $.download Plugin

A Word of Caution

Since this plugin simply appends an HTML form to the page and submits it, you do run the risk of sending the user to a new page. This could occur if the server responds with anything other than a file for download (such as an error condition), so you'll want to take that into account when using this plugin.

Thoughts? Questions? Improvements? Let us know below!

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and pre-order: Visit the book site

Comments

why not append a hidden iframe and submit the iframe? that way, if the download fails, the user will not be sent away?

Comment by Luka Kladaric on 03/02  at  08:08 PM

sorry, forgot to subscribe…

Comment by Luka Kladaric on 03/02  at  08:08 PM

@Luka: good point. we could handle the post within an iframe and eliminate the possibility that the user might leave the page. I had done this at first but found that the only way I could clean up (remove) the iframe was on a timeout, since there wasn’t really an event to tie into. Thoughts?

Comment by Scott (Filament) on 03/02  at  09:09 PM

why would you want to clean up? it’s not likely to cause a lot of issues if you just create it as a child of BODY… use an unlikely ID (#filamentgroupajaxdownload, for instance), re-use if exists, and don’t bother cleaning up…

trapping errors would be nice, but that level of hackery is way out of my league

Comment by Luka Kladaric on 03/02  at  09:14 PM

@Luka: Good point. We’ll give it some thought. I do like the idea of gracefully handling an error response without leaving the page smile

Comment by Scott (Filament) on 03/02  at  09:39 PM

Using a single iframe could cause problems.
Maybe a connection pooler must be used in this case, with timeout per connection (a connection is actually a iframe element with random generated id) smile

Comment by Lyubomir Petrov on 03/02  at  09:55 PM

You can handle it with some server side code and include a header in the HTTP response so that you can be sure the request was successful.

I’ve done a similar thing (http://www.samaxes.com/2008/10/23/stripes-and-jquery-ajax-forms-and-http-session-validation/) to check if a HTTP session has expired.

Comment by Samuel Santos on 03/03  at  08:40 PM

It will be very easy to implement a server side code, but as i get the idea of the plugin the priority is the client side.

Also implementing a server side code, will make the plugin hard to use (deploy), so it will be best if all the things are done in the jq code smile

Comment by Lyubomir Petrov on 03/03  at  08:46 PM

yeah, server-side code is a bad idea… if you need it to work properly to detect errors, you’re screwed =)

a bad solution is no better than no solution

Comment by Luka Kladaric on 03/03  at  09:22 PM

I agree. It sure is a bad solution for a jQuery plugin.

Another idea to improve the iFrame approach might be to use Cross-document messaging http://dev.w3.org/html5/spec/Overview.html#crossDocumentMessages.
This is something already implemented by some of the major browsers (Opera, FF3, IE8).

This way you can clean up the iFrame as soon as it responds to the source document.
Might it work?

Comment by Samuel Santos on 03/03  at  10:28 PM

Is there any event that is triggered when the response of a file download occurs?  Suppose you need to enable a ‘submit’ button or change the state of an element when the File Download dialog appears, how would you do this?  I’ve tried your File download function, but I don’t see any way to do this.

Comment by Ralph Bohnet on 03/13  at  01:01 PM

Just wanted to thank you. I spent way too much time trying to get around this only to see the data come back in the response instead of a download. Exactly what I needed.

Comment by EWalsh on 04/01  at  11:48 PM

You can find download and installation instructions at RA Project, along with some screenshots. It is very easy to install and set up WP Ajax Edit Comments on your blog. So, give it a try and let me know what you think of it. I find it extremely handy and valuable.

Comment by ZK@Web Marketing Blog on 06/12  at  05:04 AM

The problem here: there is no way to tell when file is ready to be saved. I was looking for download file solution with a callback function to run when the request is successfully processed. With this plugin, it’s impossible. Instead I use location.href=fileURL and fileURL sends PHP headers to make sure browser prompts to save file. That’s it!

Comment by Sam on 07/16  at  06:00 PM

Great post! Thanks for the source code!

I´ve already subscribed the feeds.

Comment by Transportadora on 08/03  at  04:57 PM

Just wanted to thank you. I spent way too much time trying to get around this only to see the data come back in the response instead of a download. Exactly what I needed

Comment by افلام اجنبيه on 08/15  at  07:08 PM

I also tried it but couldn’t use successfully with the above loops holes. Hope to see it again after needful corrections.

Comment by stock investment tips on 09/29  at  11:54 AM

Maybe I am missing something but this dosen’t work for me, It just takes me to the destination page showing the image instead of prompting with a download dialog?

Comment by webDev1 on 11/12  at  10:52 AM

Comment by prasad on 11/19  at  11:54 PM

You are a genious!! thanks you very mutch!!

Comment by mjsilva on 12/22  at  03:38 PM

Now I can catch callback from server when download file. Code in here:

$.download = function(url, data, method, callback){
var inputs = ‘’;
var iframeX;
var downloadInterval;
if(url && data){
// remove old iframe if has
if($("#iframeX")) $("#iframeX").remove();
// creater new iframe
iframeX= $(’<iframe src="[removed]false;" name="iframeX" id="iframeX"></iframe>’).appendTo(’body’).hide();
if($.browser.msie){
downloadInterval = setInterval(function(){
// if loading then readyState is “loading” else readyState is “interactive”
if(iframeX&& iframeX[0].readyState !=="loading"){
callback();
clearInterval(downloadInterval);
}
}, 23);
} else {
iframeX.load(function(){
callback();
});
}

//split params into form inputs
$.each(data, function(p, val){
inputs+=’<input type="hidden" name=“‘+ p +’” value=“‘+ val +’” />’;
});

//create form to send request
$(’<form action=“‘+ url +’” method=“‘+ (method||’post’) + ‘“ target="iframeX">’+inputs+’</form>’).appendTo(’body’).submit().remove();
};
};

Note :
- If server return Content-Disposition header is inline, not attachment then both IE and Firefox can catch onload event else only Firefox can --> if header is attachment I will catch onload event by setInterval + readyState of iframe
- Data param is passed is object (ex : {property : value, ...}), not string (Because I want it smile)
- You can pass params to callback if like but you need fake it more smile
- I tested it in IE 7, IE 8 and Firefox 3.5

Enjoy it !!!

Comment by openopen.sesame on 12/29  at  12:20 AM

I review my post and see src="j@v@script:false;" is changed to src="[removed]false;". It is important to $.download can catch callback (I don’t know why).

Comment by openopen.sesame on 12/29  at  12:27 AM

wow guys you are great.
I’ve used it with php word class and it is working great !

Comment by sallaboy on 01/05  at  07:04 PM

Nice! Just what I was looking for. Works out of the box as advertised.

Comment by Eric on 01/31  at  03:55 AM

thank you
i like this!!

Comment by muda1120 on 02/01  at  10:22 PM

Add a Comment:* required fields

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and pre-order: Visit the book site

Recently on Twitter...

PPK's rant on devs iPhone tunnel vision and why PE should be used to reach all mobile devices http://bit.ly/bvHb8Y

@filamentgroup 17 hours ago...