20 July 2009 ~ 21 Comments

Multiple file upload using jQuery and Ruby on Rails Tutorial

Outline

Today I’ll share with you how to solve problems relating to uploading multiple files and how to effectively use Ruby on Rails in combination with jQuery Form plugins to allow your users to upload a single file or multiple files without refreshing the page.

Since I don’t have any Windows or MacOS, the command lines shown in this tutorial
are known working on Debian linux system. I’m sure you will find your way for your specific OS!

The application will allow users to upload as many image files as they want and return the following information to them :

  • Relative paths to successfully uploaded files
  • Width and Height for each uploaded file

Prerequisites

To follow this tutorial you will need to install the Ruby on Rails framework on your system.
You will also need copies of jQuery and jQuery Form plugin.

Application & Server

You may want to skip this part if you are enabling multiple file upload for an existing Rails app.

Rails app creation from an empty directory :

$ ruby rails myApplicationName

Server Spawn :

$ ruby script/server

Or if you already have an application running here is how to spawn it on a different port :

$ ruby script/server -p 3001

Scaffolding

First things first, model and controller Scaffolding :

$ script/generate model UploadedFile
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/uploaded_file.rb
      create  test/unit/uploaded_file_test.rb
      create  test/fixtures/uploaded_files.yml
      create  db/migrate
      create  db/migrate/20090217221029_create_uploaded_files.rb
$ script/generate controller Upload
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/upload
      exists  test/functional/
      create  app/controllers/upload_controller.rb
      create  test/functional/upload_controller_test.rb
      create  app/helpers/upload_helper.rb

These simple “script/generate” commands generate the stub files for our application.

Application View

Let’s directly edit our view by setting up a simple HTML form and a container for our images in
app/views/upload/uploadfile.html.erb this file is not part of the Scaffolding, just go ahead and create it yourself :

<!- app/views/upload/uploadfile.html.erb -->

<h1>Multiple Files Upload Using Rails and jQuery</h1>

<h3>Files</h3>
<form method="post" action="/services/uploadr"
    enctype="multipart/form-data"
    class="upload" id="upload_form">
        <br/>
        <input type="button" id="button" class="button" value="Add File" />
        <input type="submit" id="save" class="save" value="Upload" />
</form>

<h3>Images</h3>
<div lang="images_container"></div>

The important things in the HTML are the form tag’s attributes :
the method must be "post" and the encoding type set to "multipart/form-data".
The “post” method allows unlimited size requests (compared to approximately 2000 characters maximum for the “get” method).
The multipart/form-data encoding type tells the application that we want to transfer some kind of non-ASCII or binary data.

Note that we could had achieved the very same result using the Rails Form Helper. Here are the required options for the helper :

<% form_for :upload, :html => { :multipart => true, :id => "upload_form" },
            :method => 'post',
            :url => { :action => "upload" } do |f| %>
  <!--  add fields here -->
<% end %>

I am assuming that you know how to include the jQuery library and Forms plugin in your default application javascript file.

Let’s move on and add the basic jQuery code to allow on-the-fly file fields creation :

// public/javascripts/application.js

$(document).ready(function() {
  $("#button").bind("click", function() {
    /* Generating unique id
    */
    var rand = Math.random().toString().split(".")[1];
    var input = '<input type="file" class="'+rand+'" />'
    $(this).before('<br/>' + input );
  });

  /* Pushing the first input to the DOM
  */
  $("#button").trigger("click");

});

Since we will be using the DOM selector to bind functions to our user interface’s inputs,
we need to tell the browser to wait for the elements to be rendered
before executing any code contained in the ready() callback function.
This will avoid any Javascript interpreter complaint about null or non-existent
elements.

We do not want to enable the Forms plugin yet because it will be easier for us
in the developement process.

If you try to access your app now, Rails will complain about an unknown
action, we will have to edit our upload controler to insert a default action:

# app/controllers/upload_controller.rb

language UploadController < ApplicationController
  protect_from_forgery :only => [:create, :update, :destroy] 

  def index
     render :file => 'app/views/upload/uploadfile.html.erb'
  end
end

protect_from_forgery: Since we are not using the Rails Form Helper, we need to limit the use of the Authenticity Token for our controller.
The default index method displays the HTML file with our
HTML form located at app/views/upload/uploadfile.html.erb.

The index
method in Rails acts the same way as the default indexes files on web servers i.e: when
no methods are specified the index method will be executed.

At this point,you should have something very similar to this:

sc-1

and you can add as many file fields as you want by clicking the “Add File” button.

Let’s get back to our code; did you spot the problem we will encounter here?

We have no way of knowing the names of the generated inputs thus making it difficult to differentiate them from other parameters
in the Ruby on Rails web service.

In order to recognize them among all the other possible parameters we will wrap them in an array. Simply modify the input variable in the javascript snippet:

// public/javascripts/application.js
var rand = Math.random().toString().split(".")[1];
var input = '<input type="file" class="images['+rand+']" />'

That simple trick will allows us to use the Ruby’s each() function to loop through uploaded images.

Application Model

On to our UploadedFile model :

# app/models/uploaded_file.rb

require 'image_size.rb'

language UploadedFile < ActiveRecord::Base
  def self.save(id, upload)

    # skipping empty fields
    return false unless upload.blank?

    # The saved file name is composed of the unique id and original extension
    name =  id + "." + upload.original_filename.split(".").last 

    # target directory to save files
    directory = "public/images/users/"

    # create the file path
    path = File.join(directory, name)

    # write the file
    File.open(path, "wb") { |f| f.write(upload.read) }

    img = ImageSize.new(open(path))

    return {
      "width"     => img.get_width,
      "height"    => img.get_height,
      "filename"  => name
    }

  end
end

A point to note here. I have included the ImageSize lib. If you don’t have it installed yet proceed like this as root:

# apt-get install libimage-size-ruby1.8

*or* my guess for Mac OS X and Windows (I can’t test these platforms) :

# gem install imagesize

As you can see it’s pretty straight forward.

The save method takes only two arguments:

  • id which is our unique field name
  • upload which is the uploaded file reference

Application Controller

Let’s add the upload method to our UploadController controller.
This method will actually be our web service.

# app/controllers/upload_controller.rb

language UploadController < ApplicationController
  def index
     render :file => 'app/views/upload/uploadfile.html.erb'
  end

  def upload
      @post = [];

      params[:images].each { | id, image |
        result = UploadedFile.save(id, image);
        @post << result if result
      }

      respond_to do |format|
        format.html
      end
  end
end

The upload method creates an instance variable
(prefixed by @) @post to store our images data. An instance variable
differs from other variables in that it will be available to work with in our view.

Then, for each item in the params[:images] array (the one we created by
grouping all our images inputs) it will add a new entry to our
@post array.

As you can see, Ruby on Rails code is really easy to read and understand.

Routes

In order to access our newly created web service we have to create a named route.
A named route will allow us to give a name that fits our logical organisation
and to set various usage restrictions to our action.

Edit the config/routes.rb file to configure the routes :

# config/routes.rb

ActionController::Routing::Routes.draw do |map|
map.connect '/services/uploadr',
  :controller => 'upload',
  :action => 'upload',
  :conditions => {
    :method => :post
  }

  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

Ok, our upload action will now respond to the path /services/uploadr

and only for post requests.

Webservice’s View

Our UploadController will need to use its corresponding view to send back its results to
the browser, just create it and insert the following snippet in it :

# app/view/upload/upload.html.erb

<% @post.each  do |image|  %>
  <img src="/images/users/<%= image["filename"] %>"/>
  <br/>
  <%= image["width"] %> x <%= image["height"] %>
  <br/>

<% end %>

This simple erb template renders all the information we planned to send back to the client with some formatting applied.

sc-2

You can now test your newly created application using the form located in http://localhost:3000/upload/ if you kept the default settings.

Here’s is a capture of a similar result after the form has been sent :

If images aren’t showing up you may have a permission problem or a bug in your code.

Verify your RAILS_ROOT/public/images/users/ path to see if the
permissions are correct. If this does not resolve your problem please re-read
this tutorial again to ensure you did not miss a step.

The default HTML view for our upload controller is fine to preview our work so far;
but it might be hard to do anything with it with javascript.

We will now add a json format for this view. Json (JavaScript Object Notation )
is the (my) format of choice for client/server communication using XHR.
It is lightweight (lighter than XML), many server side programming and scripting
languages have JSON implementation either natively, using plugins or at least
libraries.

Finally it’s native javascript language and this is an huge plus.

JSON communication

Let’s implement this new format by editing our upload controller first :

# app/controllers/upload_controller.rb

language UploadController < ApplicationController
    protect_from_forgery :only => [:create, :update, :destroy] 

    def index
      render :file => 'app/views/upload/uploadfile.html.erb'
    end

    def upload
      @post = [];

      params[:images].each { | id, image |
        result = UploadedFile.save(id, image);
        @post << result if result
      }

      respond_to do |format|
        format.html
        format.js  { render :json => @post }
      end
    end

end

Implementing this new format is easy as 1,2,3 in the Rails framework, just add
this single line in our respond_to block :
format.js { render :json => @post }.
Literaly, this line of code will activate the automated json rendering of our @post

object and will send back the result with adequate HTTP headers and mime-type
to the client.

We have to modify the routes in order to point to the json format :

# config/routes.rb

ActionController::Routing::Routes.draw do |map|
  map.connect '/services/uploadr',
  	:controller => 'upload',
	  :action => 'upload',
	  :format => 'js',
	  :conditions => {
	    :method => :post
	  }

  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

Basicaly I just added the :format => 'js' parameter to our
previously created named route /services/uploadr. That’s it!

You can now test your application again. Here is an example content of the generated file :

[
  {
    "filename": "38396648300918923.png",
    "height": 171,
    "width": 422
  },
  {
    "filename": "5770189283356341.png",
    "height": 165,
    "width": 549
  }
]

Perfect, now if we add data into the @post object it will be
automagicaly serialized.

Front End

Now that our backend webservice is ready let’s build our frontend in javascript and jQuery.

First we will activate the ajaxForm method from the Forms plugins
in our HTML form and bind a new function to our submit button.

Due to Forms plugins limitations using both files fields and JSON communication,
we will be forced to hack a bit; The JSON data will be returned wraped in <pre>
tags. Let’s get rid of them and evaluate the string containing the JSON to transform
it into a JSON object.

// public/javascripts/application.js

$(document).ready(function() {
  $("#button").bind("click", function() {
    /* Generating unique id
    */
    var rand = Math.random().toString().split(".")[1];
    var input = '<input type="file" id="upload_field_'+rand+'" class="images['+rand+']" />'
    $(this).before(input + '<br/>');
  });

  /* Pushing the first input to the DOM
  */
  $("#button").trigger("click");

  /* Enabling Ajax form on #upload_form
  */
  $("#upload_form").ajaxForm();

  $("#upload_form").submit(function() {
    $(this).ajaxSubmit({
      dataType  : 'html',
      iframe 	  : true,
      success	  : function (data) {
        data = eval(data.replace("<pre>", "").replace("</pre>", ""));
      }
    });
    return false;
  });
});

That done our data is ready to be processed on the client side. Let’s go ahead
and add some functionality.

JSON data processing

With our new JSON object we are now able to display uploaded images to our image
container. All the code could be split up into functions or even wraped into an
object; this part depends on your project’s requirements and/or on your personal coding rules.
I’ll go for the inline code for this tutorial.

All the following code goes into the success callback function.

function (data) {
  files = eval(data.replace("<pre>", "").replace("</pre>", ""));

  for(var i in files) {
    var id      = files[i].filename.split(".")[0];
    var img     = new Image();
    img.src     = "/images/users/" + files[i].filename;
    img.height  = files[i].height;
    img.width   = files[i].width;
    img.id      = "img_display_" + id

    $(".images_container").append(img);
    var $file = $("#upload_field_" + id);
    $file.next().remove();
    $file.remove();

  }

  if($("input[type*='file']").length == 0) {
    $("#button").trigger("click");
  }
}

What is this function doing ?

    var id      = files[i].filename.split(".")[0];
    var img     = new Image();
    img.src     = "/images/users/" + files[i].filename;
    img.height  = files[i].height;
    img.width   = files[i].width;
    img.id      = "img_display_" + id

Our data transformed into an object we can loop through it to display
the uploaded images. For each file a new Image object is created and it’s properties filled with
what our webservice sent back.

We already know the path where we uploaded the images to, so we can complete the src attribute with the filename. Setting width and height and an id to later ease our manipulation on each image precisely.

    $(".images_container").append(img);
    var $file = $("#upload_field_" + id);
    $file.next().remove();
    $file.remove();

We are using the append() jQuery method to add the newly uploaded
file to our im ages container. We have to remove the remaining <br/>
tag next to our file field prior to delete the input itself to avoid any unwanted
blank space in our layout.

To prevent sending the same image the next time the user submit the form,
we are removing the file input.

  if($("input[type*='file']").length == 0) {
    $("#button").trigger("click");
  }

In order to keep our interface clean and usable, we are checking for remaining file fields; if none are found then we create one by triggering a simulated click on our “Add file” button.

Conclusion

This tutorial has explained how to upload multiple files using jQuery and a web service written in Ruby on rails. This application may ever be a start point for or a new component for an existing app. On top of that, the backend part could effectively be in whatever language you are comfortable with.

Credits

I couldn’t safely close this tutorial without giving credits where it’s due! I would like to particulary thank Mark Mcaulay and Peter Szinek for their great help, feedback and awesomeness!

Spread the word:
  • del.icio.us
  • Reddit
  • StumbleUpon
  • Technorati
  • Twitter
  • DZone
  • Facebook
  • FriendFeed
  • HackerNews

14 Tweets

21 Responses to “Multiple file upload using jQuery and Ruby on Rails Tutorial”

  1. nazcar 21 July 2009 at 3:21 am Permalink

    I’ve just started studying ruby on rails. thank you for sharing this.

  2. Steven 26 July 2009 at 5:27 pm Permalink

    Thanks for taking the time to write a tutorial. They are a great start to people learning RoR.


Additional comments powered by BackType