AJAX File Uploads with the iFrame Method

AJAX file uploads, how do they work?! Well, they kinda don’t.

Browsers don’t allow file uploads via XMLHttpRequest (aka XHR) for security reasons. If we try to submit a form remotely via XHR, it will work, except with the file field stripped out of the request parameters. This sort of partial, silent failure can lead to unicorn black-eyes. (I punch unicorns in the face when I’m frustrated.)

This is how jQuery’s standard AJAX functions work, including .ajax().

In turn, the Rails 3 jQuery UJS driver uses jQuery’s standard AJAX functions internally. To prevent this sort of silent failure in Rails 3, we added the ajax:aborted:file event to abort the remote form submission if any non-blank file inputs are detected.

See New ajax:aborted Rails jQuery UJS Hooks.

Workarounds

So, there are a couple workarounds that give the impression of AJAX file uploads (and thus the same workflow and UI from the user’s perspective), without actually submitting the file via XHR.

One method is to use Flash. Since Flash makes connects to the server outside the scope of the browser’s connection, it essentially plays by it’s own rules.

Another, more popular method (since it works in browsers without requiring Flash to be installed) is known as the iFrame method. And here’s how it works.

The iFrame method

From here on, I’ll be referring specifically to the ajaxSubmit() method built into the jQuery form.js plugin, but the concepts are generally the same for any library that uses the iFrame method.

Assuming we have a form with a file-type input field, the iFrame method of uploading files can be summarized in the following steps:

  1. Hijack the forms submit event to execute our custom iFrame-method function (e.g. .ajaxSubmit() from the form.js plugin).
  2. Using JavaScript, create an iFrame element and insert it into the current page (and make the iFrame tiny and invisible to the user).
  3.  
    Thanks to spinn over on Reddit for catching this. I’ve updated this step to illustrate the more correct way.
    Thanks to malsup in the comments for keeping me honest here and suggesting a further clarification.

    Change the target attribute of the form, such that the results of the form submission are rendered in the new iFrame instead of the current window.

  4. Submit the form to the iFrame normal-style (non-AJAX).
  5. Allow the iFrame to navigate to the response page (since it was submitted normal-style). Note that because this happens in the iframe, the parent window does not go to a new page or get redirected, only the hidden iFrame.
  6. Copy the response content from the iFrame back into the parent window.
  7. Delete the iFrame, reset the form’s target attribute to its original value, and inform the .ajaxSubmit() (from form.js) function’s callback hooks that the submission is complete.

It’s that easy! Er, not quite…

Complications

The above steps have a couple implications to further complicate things…

Response headers and status code

Since the submission takes place normal-style in an iframe, the parent window cannot inspect the response headers or status code (from step 5 above), due to browser security restrictions.

So, .ajaxSubmit() in the parent window must assume, once the request is completed, that it was successful. There are ways around this, but as of this writing, they have not yet been pulled into the form.js plugin.

Response JavaScript not automatically executed

Furthermore, if a JS response was returned, it won’t get executed in the parent window, because it was returned to the iframe. We would thus need to manually check if the response was JS code (instead of HTML markup or some other format), and execute it ourselves in step 7 above.

Form.js has a workaround which allows you can return some javascript code inside a textarea element as the response content. So then, if the iframe response content contains a textarea with some text value once the iframe form is submitted, it will copy that textarea content back to the parent window and execute it as javascript.

AJAX file uploads in Rails 3

Now that I’ve set the stage, in my next article, I’ll show how we can seamlessly add AJAX file upload capabilities to Rails 3 by extending jquery-ujs.

Future-proof

Thanks to Gábor in the comments below for bringing this up and prompting me to add this section.

Of course, we must ask, why are the browsers making it so hard for us to do something so simple? After all, is it really making the user’s interaction any more secure if there is such a workaround, as described here, which is as useful as it is popular? The answer is, no not really. And that is why the newest browsers have started to implement an actual AJAX file upload API.

As mentioned in the comments, we can see that a few of the latest browsers are already supporting this. For an idea of how to use this API, check out Mozilla’s documentation for using it with Firefox 4.

16 Responses to “AJAX File Uploads with the iFrame Method”

  1. Gábor Farkas says:

    you can upload files using ajax, you just need a recent browser (http://caniuse.com/#feat=fileapi) . see for example https://developer.mozilla.org/En/Using_XMLHttpRequest#Sending_files_using_a_FormData_object

    • Hi Gábor, that’s a very good point. I suppose to future-proof this article, I should mention that. Thanks for the resources, I’ve add those to the article as well.

  2. malsup says:

    Nice article, Steve. Just a couple of nits…
    1. Nothing is every copied *to* the iframe.
    2. The form is not submitted to the iframe. Forms are always submitted to the server. The iframe merely receives the response.

    • Hey Mike, thanks for the feedback! I love the form.js plugin, btw, so thanks for the great work there.

      1. Nothing is every copied *to* the iframe.

      Yes, absolutely, this was a misconception on my part back when I first started writing the article. Luckily, the reddit crew set me straight on that one, and I’ve since updated the article.

      2. The form is not submitted to the iframe. Forms are always submitted to the server. The iframe merely receives the response.

      That is correct. I meant to illustrate conceptually that the form is being submitted in the context of the new iframe rather than the current window, meaning the server receives the request from the iframe, instead of the current window. By the W3C definition:

      The target attribute specifies where to open the action URL.

      So, more specifically, it’s not just that the iframe receives the response, the iframe actually sends the request too.

      I’ve updated the article to be more clear on this point. Thanks again, Mike!

      • malsup says:

        In the case of the jQuery Form plugin, the iframe does not send the request. The iframe is the target of the response, not the request. The request is sent from the main window via native browser submit.

      • Ah yes, of course form.js sets the form’s target attribute just like anything else. I had always gone by the W3schools definition of the target attribute:

        The target attribute specifies where to open the action URL.

        Which is ambiguous and seems to imply that the action URL is opened from the target. However, with a little more searching, I was able to find this clearer definition, which states:

        TARGET indicates which frame in a set of frames to send the results to…

        I submitted a suggestion for the W3schools site to maybe restate the definition of target as:

        The target attribute specifies where to open the response from the action URL.

        Thanks for keeping me honest!

  3. Mike B says:

    Nice acticle. We’ve been using this method to allow for image “previews” when uploading images using a form, with some slight variations.

    The way we approached it, was that the file prompt the user sees is actually in the iframe rather than the page, and the iframe form is submitted on the change event for the file element. The form posts the file and performs an action (in our case saves the image to a directory.) Then a Javascript function on the parent is called to report the file upload success/failure (we use this function to generate the preview image from the image written to the server and hide the iframe until the user clicks a link to change the image.)

    You can see it in practice on http://www.whatwasthere.com (coded by some fellow workmates of mine at Enlighten.) It is the Upload Photo functionality (an account is needed.) I’m currently using similar code in 2 other projects, but neither of them are live yet.

    • Amit says:

      Awesome, I really liked your idea. It is simple and very effective.

    • chaz says:

      i have tried this approach, but have hit a wall. seems i cant seem to get it to be cross browser. fixing it in one browser breaks in the next. with IE9 jquery cannot find the form element in the DOM so a ‘submit()’ is not possible. one would need to stick a (hidden) submit input/button on the form then trigger a ‘click’. unfortunately putting the hidden input on the form then breaks firefox, the jquery ‘submit()’ will no longer work(nor will triggering a ‘click’ on the button, until you remove the input/button from the iframe form.

      • I’ve experienced this same problem in IE as well. The problem is that IE submits the form data as soon as a form’s submit event is triggered, instead of propagating the event up the DOM. To rectify this, jQuery manually propagates the submit event up the DOM, and then allows the form’s own submit event to trigger.

        This works, unless of course you manually trigger a form’s submit event, because this causes the form to submit to the browser before bound events can fire.

        The work-around I used in the Rails jquery-ujs test suite is this:

        if(!$.support.submitBubbles) {
          // Must indrectly submit form via click to trigger jQuery's manual submit bubbling in IE
          form.find('input[type=submit]')
            .trigger('click');
        } else {
          form.trigger('submit');
        }
      • chazz says:

        i am trying a similar approach, but to no avail i still am stuck with either putting the button on the form or not. in IE i have to have the button on the form for anything to happen, but then Fire fox does not work. if i remove the button fire fox works again and IE does not.

        if($.browser.msie){
            var iframe = $(window.imageiframe.document.getElementsByTagName("body")[0]);
            ($("#uuid", iframe)[0]).value = data.uuid;
            //somehow the form element is NOT in the DOM. i have to go digging for the button.
            $(window.imageiframe.submit).trigger("click");
        }
        else{
            var imgForm = $("iframe#imageiframe").contents().find("form");
            $("#uuid", imgForm)[0].value = data.uuid;
            imgForm.trigger('submit');
        }

        so, in IE the form element in the iframe is NOT in the dom. so i need to put a button on the form to ‘click’ it. BUT when the button is on the form then in FF and chrome doing a jquery submit() on the form element(it is locateable in the DOM in these browsers)

      • chazz says:

        figured out my problem, seems jquery is a little fussy about finding things in an Iframe.
        apparently using

        $("form",iframe)

        will not find the form, where as

        $(window.imageiframe.document.getElementsByTagName("form"))

        will. i changed that and now the form will submit.

  4. Swathi says:

    Hi ,
    I have s:file upload tag in my jsp.and one sx:autocompleter tag.
    if i change the element in sx:autocompleter the ajax to work.
    But the s:file tag is preventing not to work ajax properly.
    Can you give me some idea.Pls…..
    Im trying this for past one week…..
    Thanks in advance.

  5. sandeep says:

    the response html text for ajax submit is coming in encripted format when we are uploading file using ajaxsubmit.
    can any one help me in this

  6. François says:

    Thanks for this article, it is really well made :)

Leave a Reply

You may include code snippets in your comment using this syntax.