Esteban Pastorino

Simple File Uploads with Backbone.js

Backbone.js is great for doing single page apps or to tidy up some messy javascript code. But there are a few things that are a bit tricky to do, like uploading files. Sure, some browsers support uploading files via XHR, but some others don’t (I’m looking at you IE). There are a few workarounds for this situation. Here’s a quick explanation on how I achieved file uploads in backbone in a recent project.

Setup

We’ll be using the old iframe trick. It basically consists on posting a form to a hidden iframe and reading back the content once it loaded. You can read a bit more here or here.

Implementing this from scratch could be quite some work. Fortunately, there’s a jQuery plugin to save us some time and headaches.

About the plugin

It adds 2 options to the $.ajax function: iframe and files.

Quick example
1
2
3
4
$.ajax('/some/path', {
  iframe: true,
  files: $("form :file")
});
  • iframe expects a boolean, set it to true when you wan’t to upload files.
  • files expects a jQuery object of one or many file fields.

What it will do is exactly the trick mentioned above. Will set up a new iframe, a new form with the target attribute set to the name of the iframe, and then just listen to the load event on the iframe.

There are a couple of extra things good to know when using this plugin:

  • It adds a X-Requested-With=IFrame value to the form. That’s useful in case you need to do any special processing in the backend.
  • If you’re sending along other data with the request, you must need to send it in JSON form, it does not accept an already serialized data string. That’s because it creates a new hidden input field inside the form for each value it submits.

A bit about Backbone

To understand what we’re going to do, we first need to know a few things about how Backbone works.

In this case, we’re interested in the options argument of model.save1.

Under the hood, save calls sync to communicate with your server, passing along the options object (with a few modifications). We’re going to take advantage of that.

Submitting

To illustrate this quick example, imagine 2 simple parts of the app.

A (simple) form with a file field
1
2
3
4
5
6
7
8
9
10
11
12
13
<form enctype="multipart/form-data">
    <label>
      File
      <input name='file' type='file' />
    </label>
    <br>
    <label>
      Name
      <input name='name' type='text' />
    </label>
    <br>
    <input type="submit" value="Upload">
</form>
A (simple) Backbone.View tied to the HTML above
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var FormView = Backbone.View.extends({

  // some more code here

  events: {
    'submit form' : 'uploadFile'
  },

  uploadFile: function(event) {
    var values = {};

    if(event){ event.preventDefault(); }

    _.each(this.$('form').serializeArray(), function(input){
      values[ input.name ] = input.value;
    })

    this.model.save(values, { iframe: true,
                              files: this.$('form :file'),
                              data: values });
  }
});

When the form is submitted we’re are going to serialize all form’s data, and save it to the model. And we’re gonna add a few extra options (iframe, files, and data), that will get passed to the sync method, and finally to the $.ajax call.

As we saw before, iframe: true means that the request is going to use the iframe transport. The files option, is set to the form files fields. And finally, we need to specify data values again. That should be the same data we want to persist on the model, but as explained above, the iframe transport methods needs it to be a JSON object.

Caveats

That’s already working! Quite easy, except for a few quirks we need to be aware of.

Always resolves as success

As the plugin listens to the load event of an iframe, it can’t know the HTTP status code of the server response. That means, it can’t tell wether the response was a 2xx, 5xx, or any other thing. All calls will be triggering a done() or success() callback, never a fail() one.

You should determine if it was OK or not by looking at the response payload. It’s wise to return a JSON with some message if the response wasn’t successful.

Unknown Response Data Types

Again, because it just listens to a load event, it does not have access to the HTTP headers of the server response. This makes it harder to use the automatic content-type detection provided by jQuery.

Fortunately, in this case there’s a simpler workaround: Wrap the data in a <textarea> element with a data-type attribute specifying the content type.

1
2
3
<textarea data-type="application/json">
  {"ok": true, "message": "Thanks so much"}
</textarea>

This is specially important when using IE. It will trigger a download alert if it can’t detect the content type.

Using it with Rails

I’ve used this solution with a Ruby on Rails app, and found a few more things worth noting.

Authenticity Token

If you’re using protect_from_forgery on your controllers, there’s a good chance that you need an extra tweak to make this work. That is, passing the authenticity_token as a parameter too.

Just modify the uploadFile function a bit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  uploadFile: function(event) {
    var values = {};
    var csrf_param = $('meta[name=csrf-param]').attr('content');
    var csrf_token = $('meta[name=csrf-token]').attr('content');
    var values_with_csrf;

    if(event){ event.preventDefault(); }

    _.each(this.$('form').serializeArray(), function(input){
      values[ input.name ] = input.value;
    })

    values_with_csrf = _.extend({}, values)
    values_with_csrf[csrf_param] = csrf_token

    this.model.save(values, { iframe: true,
                              files: this.$('form :file'),
                              data: values_with_csrf });
  }

It’s kinda ugly, but still better than to remove the CSRF-protection from your controller.

Middleware with textarea wrapping

And finally, to make it easy to wrap the response in a textarea when needed, there’s a nice middleware. It was created by the author of the jquery-fileupload-rails, which uses the iframe transport under the hood.

Go take a look at it. It’s quite handy!

Closing

Uploading files with Backbone via ajax while maintaining compatibility for older browsers can be a thing. Hope this helps somebody!


  1. save can receive 2 or 3 arguments: (attributes, options), or (key, value, options).

Comments