Upload Files with Backbone.js, Node.js and express 4.x

Backbone fundamentals is a great free resource to learn Backbone.js from scratch. The book was written by Addy Osmany under creative-commons license. As its second exercise, the book guide the readers to create a simple library application that uses Node.js as the back-end. However, it left the part to upload book’s cover to the readers as an exercise. Hence, here is the way I did it.

Requirement

There are two additional requirements for the upload book’s cover features: - The selected cover should be previewed as thumbnail

This implies that there should be a space to show the selected image. When users change the image, the preview should change accordingly.

  • Upload process shall happen only when a new book is added

The upload happens if and only if when users click the button to add a new book. This signifies that the displaying the cover’s preview should not upload to image file to the server.

Problems

After browsing for awhile, I found a blog post to upload file asynchronously using Node.js and express, which was good as a starting point. However, similar to most of online references I had found, they were pretty much obsolete; most of them were using express <= 3.x that supported file upload by using body-parse middleware (as mentioned in the blog post) where in express 4.x the body-parse middleware did not support file upload any longer.

Another problem was to address the requirement: previewing images without upload them to the server in the first place. This was tricky, as most of the solutions were to have the images uploaded first then fetch the images’ URL to be previewed.

Solution

I had to admit, the first problem is another RTFM problem. So when I read again body-parser’s documentation it was written clearly that body-parse did not handle multipart bodies (file uploads). Furthermore, it mentioned the alternatives modules to handle multipart bodies, and one of them is multer.

Just like another express middleware, I needed to tell express to use multer and specified to which directory the files will be uploaded as shown in the following coffeescript code (yes, I wrote the back-end using coffeescript).

express = require 'express'
multer = require 'multer'

app = express()
app.use multer( { dest: "#{__root}/public/img/covers/" } )

For the second problem, I found out that javascript provides a FileReader object whose capable of reading file from client’s machine, which could be used to load a selected image from a browser locally.

The following code is a Backbone view to handle the feature to display the selected image. The main idea is to catch the change event from an <input type="file"> and read the file and render it through the designated <img> element and later on to upload the file as well.

app.ThumbnailView = Backbone.View.extend({
  events: {
    'change #coverImageUpload': 'renderThumb',
    'submit #uploadCoverForm': 'upload'
  },

  render: function () {
    this.renderThumb();
  },

  renderThumb: function () {
    var input = this.$('#coverImageUpload');
    var img = this.$('#uploadedImage')[0];
    if(input.val() !== '') {
      var selected_file = input[0].files[0];
      var reader = new FileReader();
      reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
      reader.readAsDataURL(selected_file);
    }
  },

  submit: function () {
    this.$form = this.$('#uploadCoverForm');
    this.$form.submit();
  },

  upload: function () {
    var _this = this;
    this.$form.ajaxSubmit({
      error: function (xhr) {
        _this.renderStatus('Error: ' + xhr.status);
      },
      success: function (response) {
        _this.trigger('image-uploaded', response.path);
        _this.clearField();
      }
    });
    return false;
  },

  renderStatus: function (status) {
     $('#status').text(status);
  },

  clearField: function () {
    this.$('#uploadedImage')[0].src = '';
    this.$('#coverImageUpload').val('');
  }
});

In details, when a user has selected a cover image, the 'change #coverImageUpload': 'renderThumb' event will be triggered. To add a bit context, #coverImageUpload is the id of the <input type="file"> to upload a file and renderThumb is the function will be executed as the event’s callback. In the function, whenever a user selected a picture, the view will get the selected file and read the file as data URL through FileReader.readAsDataURL function. When the particular function is executed, it triggers FileReader’s onload event with the result of the data reading process as its callback’s parameter, which is used as the image source of the <img> element as shown in the listing above.

The uploading part was a bit tricky. I used Backbone View’s event to make sure that the newly added book has the right cover image. The way to do this is to make sure when a user clicks the add book button, the cover image will be uploaded first and when the the upload success an event will be triggered with the server path of the uploaded image as the parameter. Then the path will be used as the value of <input> related to the book cover. The last step is to create the book object in the Backbone Collection, which will be sync’d to the Node.js back-end server. The following sequence diagram pictures the description above.

sequenceDiagram
  User->>LibraryView: click add book button
  LibraryView->>ThumbnailView: upload
  ThumbnailView->>ThumbnailView: trigger('uploaded', response.path)
  opt uploaded event
    LibraryView->>LibraryView: updateInput
    LibraryView->>LibraryView: createData
  end

To enable the event in the LibraryView, the object needs to listen to to the ThumbnailView.

app.LibraryView = Backbone.View.extend({
  ...

  initialize: function (initialBooks) {
    this.collection = new app.Library(initialBooks);

    this.thumbnailView = new app.ThumbnailView();
    this.bookListView = new app.BookListView( { collection: this.collection } );

    this.listenTo(this.thumbnailView, 'image-uploaded', this.updateInput);
  }
}

And that’s all folks! I hope this tutorial could be a help for someone who looks for the solution for the exercise. Please see the project’s repository for the complete solution.

Hello, my name is Nauval. I like building/crafting things. I code for living. I blog in my spare time.

Have a look around and don't hesitate to contact me with any comments, suggestions or even to just say Hi!