Ruby
Article
By Vahob Rasti

Asynchronous Multiple File Upload with Rails and Dropzone.js

By Vahob Rasti

Screenshot 2016-08-06 14.36.57

In this tutorial, I am going to show you how to use Ruby on Rails 4.2 and Dropzone to upload multiple files using drag and drop with AJAX. Dropzone is an awesome library that, in this author’s opinion, stands head and shoulders above the rest of the libraries that offer similar functionality.

Note that I am going to use plain Ruby on Rails without any other library or gems such as Paperclip or Carrierwave.

This tutorial is suitable for those who are coming from other programming languages and wish to explore Rails.

OK folks, let’s hit the road.

Prerequisites

I assume that:

  1. You know a bit about Object Oriented programming and how the three components of the MVC design pattern relate to each other.
  2. You are familiar with javascript, jQuery, and AJAX requests.

Tools We’ll Need

  1. Good browser debugging and introspection tools. I am using Firefox as the browser with Firebug installed to debug and control the interactions between javascript in the browser and Rails on the server.
  2. Any IDE or editor. I like either Rubymine or Sublime is fine, but Feel free to use your own.
  3. Ruby on Rails 4.2. To install Rails, visit (http://installrails.com/). I am going to use SQLite as the database. You can also use PostgreSQL, MySQL, or any other RDBMS.
  4. The Dropzone library itself. Go and download it from here.
  5. Twitter Bootstrap.
--ADVERTISEMENT--

The Application

Our application is a module of an e-commerce shop to create products and attach pictures to them. It uploads the pictures on the server, returns the list of uploaded files, and then submits the form to create the product record. There is a one-to-many association between pictures and product, meaning that a product may have many images.

Here is the database schema:

38329356-1acc-11e6-85ef-39a73b823cb7

Let’s jump to our code. First, make a new Rails application by opening your terminal and typing:

rails new myapp

We need to add the Javascript and CSS files for Dropzone and Bootstrap, so please go to the dist folder from the Dropzone and Bootstrap downloads and find the following files:

bootstrap.css
dropzone.css
basic.css
dropzone.js

Put dropzone.js into myapp/app/assets/javascripts and all the CSS files into myapp/app/assests/stylesheets. Open application.css and remove this line:

*= require_tree .

This line tells Rails to require everything in this directory and all subdirectories. We don’t want this to happen, so get rid of it. Then add this line:

*= require bootstrap

Which means to include bootstrap for our application. Close and save the file.
Go to myapp/app/assets/javascripts, open application.js, and remove the following lines:

//= require turbolinks
//= require_tree .

For further information about //= require turbolinks please read this article. We aren’t going to use Turbolinks today. Save and close the file. Now go to app/views/layouts/application.html.erb and make a layout template for your app. Your final file should look like:

<!DOCTYPE html>
<html>
<head>
  <title>Myapp</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
  <%= yield(:css) %> <!-- We use this section for our CSS files -->
</head>
<body>

<%= yield(:content) %><!-- For our main contents -->
<%= yield(:javascript) %><!-- We put our JavaScript tags here -->
</body>
</html>

Note that we have yielded three sections: css, content, and javascript. Open the terminal and enter the following command:

rails g controller Product new create

This will create two views in your app/views folder and a controller in app/controllers. Before moving on, go to myapp/config/initializers/assets.rb and add this line to the file:

Rails.application.config.assets.precompile += %w(dropzone.js basic.css dropzone.css)

to ensure that our third-party assets are precompiled with the rest of the app.

Now, I am going to write the client-side before moving on to the server-side implementation. Open app/views/new.html.erb and enter the following code:

This section includes the CSS files, basic.css and dropzone.css, which will be used for our drag and drop area:

<%= content_for(:css) do %>

<%= stylesheet_link_tag 'dropzone' %>
<%= stylesheet_link_tag 'basic' %>

<% end %>

Here, we include a form for the creation of a product and a drag-and-drop zone which lets the user to drag images:

<%=content_for(:content) do %>
  <div class="container">
    <div class="row">
    <p>This is the form for creation of your product.</p>
      <%= form_for :request, :url => request.base_url+'/product/create', html: {id:'myForm'} do %>
      <label for="name">Product Name:</label>
      <input type="text" name="name" id="name" class="form-control">
      <label for description> Description</label>
      <input type="text" name="description" id="description" class="form-control" /><br>
      <input type=hidden name="files_list" id='fileslist'>
      <!-- We use this <div> element to initialize our Dropzone -->
      <div id="mydropzone" class="dropzone"></div> 
      <!-- This <div> elements shows a suitable message after a successful upload. -->
      <div id="msgBoard"></div>
      <br>
      <input type='submit' value="Create your product">
    </div>
  </div>
  <% end %>
<% end %>
<%= content_for(:javascript) do %>
<!-- include the dropzone library itself. -->
<%= javascript_include_tag "dropzone" %>

Here is the Javascript necessary to handle the drag and drop events and upload the form. I will explain each portion of the code as we go through it:

<script type="text/javascript">
  var AUTH_TOKEN=$('meta[name="csrf-token"]').attr('content');

Grab the CSRF token by jQuery and save it in AUTH_TOKEN variable:

  Dropzone.autoDiscover = false;
  var myDropzone = new Dropzone("div#mydropzone",{
    url: "<%= request.base_url %>/uploadfiles",
    autoProcessQueue: false,
    uploadMultiple: true,
    addRemoveLinks:true,
    parallelUploads:10,
    params:{
      'authenticity_token':  AUTH_TOKEN
    },
    successmultiple: function(data,response){
      $('#msgBoard').append(response.message).addClass("alert alert-success");
      $('#msgBoard').delay(2000).fadeOut();
      $('#fileslist').val(response.filesList);
      $('#myForm').off('submit').submit();
    }
  });

Initialize our drag and drop file uploader:

  • url is route which handles the task to upload the images on the server
  • autoProcessQueue: false stops the script from automatically uploading images after dragging them
  • uploadMultiple: true allows Dropzone to send multiple files in one request
  • addRemoveLinks: true If set to true, a Remove link will be added for each file preview
  • parallelUploads: 10 sets the number of parallel uploads allowed to 10
  • params allows us to submit additional data with the request. Here we need to pass CSRF token. Before moving on, let’s discuss some details around sending AJAX requests in a Rails application. As you probably know, if you don’t include the CSRF token with requests sent to Rails, you will get this error in your Firebug console:

    ActionController::InvalidAuthenticityToken in ProductController#upload
    

In fact, it looks just like this:

50dbced4-423d-11e6-8694-762691e879e9

So, we pass the CSRF token here.

  • successmultiple: function(data,response): If the upload process is successful on the server, then we handle the data and response by this callback function. This function adds a message to our msgBoard div, fades that message out, adds the uploaded files to a hidden input in the form, then submits the form.

Once the form has been submitted, this script checks if the list of files in the dropzone area is empty. If there are any files to upload, it sends a POST request to the server and uploads them (myDropzone.processQueue()).

$('#myForm').submit(function(e){
  if(myDropzone.getQueuedFiles().length > 0){
    e.preventDefault();
    myDropzone.processQueue();
  }
});
</script>
<% end %>

In this Rails application, we need to prevent the form submission in the first place until we make sure that all the files in the drop zone are uploaded. Once all files have been uploaded, then we can submit the form. Therefore, in the callback function for successmultiple, we let the submit event happen by off method from jQuery.

The following line submits the form if the script successfully uploads our pictures and the server sends the success message from the server:

`$('#myForm').off('submit').submit();`

Now, we need to implement the server-side of our application, which sends a success message along with the list of the uploaded files. Open up config/routes.rb and add this line:

post 'uploadfiles'=>'product#upload'

Then go to myapp/public and create a uploads directory with the following command:

mkdir uploads

This is the directory that will hold our uploaded files.

In app/controller/product_controller.rb, define the upload method as follows:

def upload
  uploaded_pics = params[:file] # Take the files which are sent by HTTP POST request.
  time_footprint = Time.now.to_i.to_formatted_s(:number) # Generate a unique number to rename the files to prevent duplication

  uploaded_pics.each do |pic|
    # these two following comments are some useful methods to debug
    # abort pic.class.inspect -> It is similar to var_dump($variable) in PHP. 
    # abort pic.is_a?(Array).inspect -> With "is_a?" method, you can find the type of variable
    # abort pic[1].original_filename.inspect
    # The following snippet saves the uploaded content in '#{Rails.root}/public/uploads' with a name which contains a time footprint + the original file  
    # reference: http://guides.rubyonrails.org/form_helpers.html
    File.open(Rails.root.join('public', 'uploads', pic[1].original_filename), 'wb') do |file|
      file.write(pic[1].read)
      File.rename(file, 'public/uploads/' + time_footprint + pic[1].original_filename)
    end
  end
  files_list = Dir['public/uploads/*'].to_json #get a list of all files in the {public/uploads} directory and make a JSON to pass to the server
  render json: { message: 'You have successfully uploded your images.', files_list: files_list } #return a JSON object amd success message if uploading is successful
end

On the server-side, we take the files sent through POST request with the params hash. Then, generate a unique numeric string which is about to be used for making the file names unique. By doing so, we prevent duplication.

Then, we upload and rename the files one by one. Finally, get the list of all uploaded files and return a JSON with a success message and a list of all successfully uploaded files.

OK, time to run our application and see what we have already done. Go to the myapp folder and start the server:

rails s

In the browser, go to the new product page. You should see the following form:

If you submit the form right now, you will upload files into the public/uploads directory. We need to create a product and associate these images to this product. Let’s create our models and their migration files.

First, create the model and the migration for our product. The product has a name and a description as well as an id. Open your terminal and enter:

rails generate model Product name:string description:text

Second, create the pic model and its migration file by entering the following command:

rails generate model Pic product_id:integer:index name:text

Go to the myapp/db/migrate folder and find the migration file for Pic model. Open it and add a foreign key with this piece of code:

add_foreign_key :pics, :products

The change function of your migration file should look like as follows:

def change
  create_table :pics do |t|
      t.integer :product_id, index: true
      t.text :name

      t.timestamps null: false
    end
  add_foreign_key :pics, :products
end

Time to setup the association between the Product and Pic models. This task is unbelievably simple in Rails applications. In the real world, we would say each product has many pictures and each picture belongs to a product. So, open your Product model and add this piece of code:

has_many :pics

Your model now looks like to:

class Product < ActiveRecord::Base
  has_many :pics
end

Because each picture belongs to a product, you need to add this line to your Pic model:

belongs_to :product

That’s it. Time to migrate with rake:

rake db:migrate

As for the final part, the application should create a product after uploading the files and associate this product to the images which are already uploaded by our drop zone. This task will be handled by create method in ProductController. Open up app/controllers/product_controller.rb.

The create method should create the product for us, make the one-to-many association, and move the uploaded file to a folder which is specifically created for the given product. The folder name is the id of the product. Also, the method should create a flash message if the operation is successful and redirect the user to the previous page:

def create
  files_list = ActiveSupport::JSON.decode(params[:files_list]) 
  product=Product.create(name: params[:name], description: params[:description]) 
  Dir.mkdir("#{Rails.root}/public/"+product.id.to_s)
  files_list.each do |pic|
    File.rename( "#{Rails.root}/"+pic, "#{Rails.root}/public/"+product.id.to_s+'/'+File.basename(pic))
    product.pics.create(name: pic)
  end
  redirect_to product_new_url, notice: "Success! Product is created."
end

The flash message will be shown on the top of creation page, so we need to make a div element which show the creation message to the user. Add the following code to your app/views/product/new.html.erb file.

<% if flash.notice %>
  <div class="alert alert-success"><P><%= flash.notice %></p></div>
<% end %>

OK, we are done with this small module for our e-commerce application. Start the rails server (rails s) and go to the new product page in the browser. If you fill out the form, drag and drop its pictures, and click the submit button, then the application will upload the pictures, create our product, and associate the uploaded images to our product.

For example, I am going to create a bike product and upload three pictures of it along with a name and a description. Here is what the new product page looks like:

If I click the “Create your product” button, it will create the record and redirect me to the same page with a flash message as follows:

Conclusion

OK, that is all to create this small module for our online shop. Dropzone.js has allowed us to equip our Rails application with a nice user experience for uploading images without having to lean on a file-uploading gem.

I hope you found this useful.

  • Werner Laude

    Thanks for the nice tut..
    Yet I cannot manage to upload. It shows the file in the field, after submitting nothing happens. => NoMethodError in ProductController#upload undefined method `original_filename’ for nil:NilClass Extracted source ..any idea?

    • vahob

      Hi Werner
      Thanks for the comment.
      Which version of rails are you using? Try to find and make sure that the POST request is successful. Right after this line :

      time_footprint = Time.now.to_i.to_formatted_s(:number)
      add this line to see what is going on with your request.

      abort uploaded_pics.inspect

  • Narek Mikayelyan

    Hello dear author, please give me a link to download the script. To me it is very necessary. Thank you

    • vahob

      Hi Narek
      which version of Rails are you using? 4.2 or 5? Because this has been written for 4.2.
      Cheers

      • Narek Mikayelyan

        Hello, I wont to organize this script on php, dropzone and mysql, like this script

        Add product

        Title

        Description

        Submit

        How can I organize on this script server and client’s part in order to connect two mysql tables so that in one there will be title and description and in other upload photos data. And these two tables will be connected by id?
        If you can help me, please write to me.

  • Khabab Hassan

    Hi
    i am php developer i use drop zone script for file uploading. I want files uploading asynchronously. If user select multiple files and click on submit button than all selected files will be uploaded in background. User don’t wait for file uploading
    Regards

    • vahob

      You need to turn on autoProcessQueue:true

      In this way, files will be uploaded immediately when the user drops them.

      • Khabab Hassan

        Hi
        Thanks for your response. I set this option my problem is that if user select multiple files thats sizes are more than 2 MB its take time during uploading. I want user select these files click on submit button that will take them to new page but images uploading process will continue in background. User don’t like to stay on that page until images are uploading.
        Regards

        • vahob

          Well , i think this is not possible at all. The files need to be transferred to the server. Even the most famous file hosts like Dropbox does not do this since it simply is not possible. You cannot leave the process in the middle. Right?

          • Khabab Hassan

            okay thanks for your response
            Regards

  • Khabab Hassan

    Hello
    I want to upload just one image instead of multiple images by using drop zone image uploader how can i do that.
    Regards

    • vahob

      thanks for your comment. For one image, you’d better use a simple input tag. This is suitable for multiple files. However, if you insist:
      uploadmultiple: false
      also change succesMultiple to success

  • Narek V Mikayelyan

    Hello dear author, how to change the script in PDO PHP

    • vahob

      Hi Narek
      Thanks for the comment
      Which DB engine are you using?The only thing you need to do is handling the file on the server side with super global array $_FILES and inserting record with PDO into your DB engine.

      • Narek V Mikayelyan

        I’m trying to do, but I did not get

        • vahob

          You already implemented the logic in the wrong way. I’ll fix it tonight (Sydney’s time) and will come back to you.
          Cheers

          • Narek V Mikayelyan

            Thank you dear Vahob for such great support, but I have found a mistake in the scrpt, while adding one file everythink is ok, but while adding in the next time another file all files that are in the upload folder are doawnloading in the base, i.e. by adding a new image all images that are in the upload folder are adding in database.In my opinion in the second part of upload.php is a mistake.

          • vahob

            you should change create.php file and move all the files alrady there while creating a new record. if you cannot do it, i’ll do it tonight .
            cheers

          • Narek V Mikayelyan

            Please, if it is possible help to finish. Thank you for everythink

          • Narek V Mikayelyan

            Please, help me to finish this script.

  • Narek V Mikayelyan

    Thank you for all. When I want to send form without picture the Add button is not active. How can I send form data without images?, I tried many metods wiith this script

    Dropzone.options.myDropzone = {

    // Prevents Dropzone from uploading dropped files immediately
    autoProcessQueue: false,

    init: function() {
    var submitButton = document.querySelector(“#finish”)
    myDropzone = this; // closure

    submitButton.addEventListener(“click”, function(e) {
    e.preventDefault();
    myDropzone.processQueue(); // Tell Dropzone to process all queued files.
    });

    // You might want to show the submit button only when
    // files are dropped here:
    this.on(“addedfile”, function() {
    // Show submit button here and/or inform user to click it.
    });
    this.on(“sending”, function(file, xhr, formData) {
    formData.append(“action”, “value”); // Append all the additional input data of your form here!
    });
    this.on(‘success’,function(d,r){
    $(“#pics”).attr(‘value’,r);
    });
    this.on(“queuecomplete”, function (file) {
    $(“#myForm”).off(‘submit’).submit();
    });
    }
    };

    , but not without any result.

  • Narek V Mikayelyan

    Hello dear author, please help in one problem with this script, when I want to upload first file the size of which is bigger than the maximum level size mentioned in the script, and in this case an automat quiry is sent to database, but when a big file is uploadind as a the second file everything goes with no problem, How can I correct it?

  • andres

    Hi guys, in the dropzone(js) code I had to change a part of the code: here I leave the line that I should modify:

    $(‘#fileslist’).val(response.files_list);

    The problem is the json var (files_list). The controller response using “files_list” var and not “filesList”.

    Greeting.

  • Maxence Malbois

    Hi !
    Thanks for this tutorial, very helpful for uploading images belonging to another model than the form model.

    Though one question regarding ajax / dropzone :
    When I drag n drop my images, they all appear in the box as thumbnails but nothing seems to happen.
    It’s only when i submit the form that the images seem to be uploaded and recorded. (The blue uploading bar of all images complete at the same time after submit)

    Is it normal ? I would think they would be uploaded one by one even though the form is not submitted

    I have tweaked your code and not implemented everything (and using paperclip on top of that), can you confirm this is the normal behavior or something is wrong my side ?

    Many thanks

    • vahob

      Hi
      Set this line to true. When you drop your image, it will be uploaded automatically.
      autoProcessQueue: false

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.