Lessons Learned from jQuery File Upload

I recently implemented jQuery File Upload on a site as a replacement for an older, considerably more bloated file upload component they had been using. There were a couple of not immediately obvious issues I ran into, and I thought I’d write them up here quickly in case it saves anyone else some time.

The Iframe Transport Is Not Completely Transparent

The most important issue to be aware of with the iframe transport (used most notably for IE) is that the returned response is only available in callbacks added directly to the fileupload widget, not to callbacks added to any returned XHR object.

In other words, this code will work as expected:

$('#fileupload').fileupload({
  dataType: 'json',

  add: function(e, data) {
    data.submit();
  },

  done: function(e, data) {
    // assuming the server returns a result like {"message": "some message here"}
    alert(data.result.message);
  }

});

This code, however, will not work when the iframe transport is used, in spite of the fact that jQuery File Upload’s wiki includes a similar example:

$('#fileupload').fileupload({
  dataType: 'text',

  add: function(e, data) {
    var xhr = data.submit();
    xhr.success(function (result, textStatus, jqXHR) {
      var response = $.parseJSON(result.responseText);
      alert(response.message);
    });
  }

});

The second example will work when XMLHttpRequest is used for file uploads (current versions of Firefox, Safari, and Chrome), but not when the iframe transport is used (all other browsers, especially Internet Explorer). The success function will be called in all cases, but the response will be empty when the iframe transport is used.

When the dataType option is set to text, json, html, or script, the iframe transport performs some processing of the response. Because it is operating on a DOM object obtained from the iframe it uses, however, and not the raw HTTP response data, there is the potential for some surprises to pop up.

Surprises are definitely a possibility if you are trying to return a text result that contains HTML markup. It may be influenced by Content-Type headers returned from the server and individual browser behavior when it comes to building a DOM around such a response.

As a very simple example to illustrate the point, let’s assume the server returns the string "<html><body>Hello world!</body></html>". And that, our code does something like:

$('#fileupload').fileupload({
  dataType: 'text',

  add: function(e, data) {
    data.submit();
  },

  done: function(e, data) {
    $('textarea').val(data.result);
  }

});

It’s a bit of a contrived example for a file upload, but in this case you would get different results depending on which transport you used. For the XHR transport, your text area would show HTML markup in the result and for the iframe transport it would not.

It is possible to tell if the iframe transport has been used by looking at the dataType setting returned with the result. So you could alter your done callback to strip out the HTML markup in both cases:

$('#fileupload').fileupload({
  dataType: 'text',

  add: function(e, data) {
    data.submit();
  },

  done: function(e, data) {
    if (data.dataType == 'iframe text')
      $('textarea').val(data.result);
    else
      $('textarea').val($(data.result).text());
  }

});

Again, this is rather contrived to illustrate the point. I suspect you could actually come up with a more elegant solution to the problem by playing with the Content-Type header returned by the server and the dataType setting you pass to fileupload to adjust what you get back. I didn’t actually have to deal with this issue, it was just something I noticed while debugging other response issues I was troubleshooting.

File Type Input Element Styling

The other issue I spent some time wrangling was styling of the <input type="file" /> element. If you use just the fileupload and iframe-transport plugins as I did, there is no styling added to file input elements or upload behavior added to other buttons.

If you want, as I did, to turn a custom button in your UI into a trigger that opens a file upload dialog, there’s pretty much one way that’s accomplished: position an <input type="file" /> element on top of your custom button and make it transparent. So, when people are using your button, they’re actually triggering the browser-provided file input element that’s sitting on top.

That seems really simple in theory, but, of course, practice introduces a number of different complications. File input elements have attached text boxes in some browsers but not in others, and they respond very differently to styling across browsers.

To get an idea of how to tackle all of that, let’s look at two relevant CSS rules used in the jQuery File Upload demo:

.fileinput-button {
  position: relative;
  overflow: hidden;
  float: left;
  margin-right: 4px;
}

.fileinput-button input {
  position: absolute;
  top: 0;
  right: 0;
  margin: 0;
  border: solid transparent;
  border-width: 0 0 100px 200px;
  opacity: 0;
  filter: alpha(opacity=0);
  -moz-transform: translate(-300px, 0) scale(4);
  direction: ltr;
  cursor: pointer;
}

Essentially what’s happening here is that the input element is, obviously enough, being absolutely positioned inside of a relatively-positioned element to control it’s location. To overcome the problems of whether or not a text field is coming along for the ride, the author is basically just making the element really big, aligning the right side with the right side of the containing element (since the button will be to the right of any text field that’s present), and then using overflow: hidden on the containing element to crop out everything that falls outside of its bounds. That pretty much guarantees there will be a nice big button that we’re pushing when we touch anywhere on that containing element.

There are a couple of curious things to take note of in the CSS. For most browsers, the button is being made very large with the addition of a very thick border on the bottom and left sides. For Firefox, however, the author is using the browser-specific -moz-transform property to literally scale it up. I’m going to assume they’ve done their homework and found the border trick to work well across most browsers.

For Firefox, there may be an alternate way to style the element if you want to avoid browser-specific rules. At least in the current version of firefox, the button itself will respond to height and width properties specified in CSS. The real issue, however, seems to be that positioning it with a value of right: 0 lines up the right edge of the text field with the right edge of the container instead of the button, which isn’t what you want. You could potentially place it inside a larger container with right text alignment and absolutely position that larger container instead. That would give more reliable results, albeit with increased complexity. I stuck fairly close to the CSS that came with the demo for simplicity. If you decide to deviate from it, all I can suggest is increasing the opacity while you’re working on it so at least you can see what’s going on.