Just another Web Developer blog

From the Blog

Feb
13

Multiple File Uploads Using jQuery and Paperclip

Posted by Cameron on February 13th, 2011 at 2:58 pm

File uploads is something that is used in Web Applications all of the time. It is becoming somewhat of a “standard” feature in many applications. When using rails, there are a number of ways that this feature can be achieved. Originally, I used attachment_fu and followed a tutorial (which I have since lost) that described how to perform multi-file uploads. This worked wonders. However, attachment_fu has somewhat fallen behind, or perhaps is no longer even supported. I have since started using Paperclip, as it seems to be the most common file upload plugin used for Rails and is very well supported.

The second element of this tutorial is jQuery. jQuery is one of the most popular javascript library’s that is used on the web. Furthermore, it is even easier to use with rails since the upgrade to Rails 3 and the release of the jquery-ujs gem. It only seems natural that someone would want to use Paperclip and jQuery together, and this tutorial shows you how.

Requirements

  • Rails 3
  • Intermediate knowledge of Ruby on Rails
  • Intermediate knowledge of jQuery and Javascript

Paperclip

The setup of Paperclip is almost standard, however we are going to modifying one or two things to allow for multiple file uploading. Firstly, install the gem.

#gemfile
gem "paperclip"
#console
bundle install

We are going to call our Paperclip attachments “Assets”. Here is our setup process.

#generate scaffold
rails g scaffold asset data_file_name:string data_content_type:string data_file_size:integer attachable:references
#create_assets.rb migration
#change
t.references :attachable
#to
t.references :attachable, :polymorphic => true
#add index after create_table
add_index :assets, [:attachable_id, :attachable_type]
#asset.rb model
class Asset < ActiveRecord::Base
#path is generally set by default, but i had to set mine
has_attached_file :data, :url => "/assets/:id", :path => ":rails_root/public/system/assets/:id/:basename.:extension"

belongs_to :attachable, :polymorphic => true

#Set number to the Max Attachments allowed for owner
Max_Attachments = 5
Max_Attachment_Size = 2.megabyte

def url(*args)
data.url(*args)
end

def name
data_file_name
end

def content_type
data_content_type
end

def file_size
data_file_size
end
end

#in the model that is going to have the attachments added to it.
has_many :assets, :as => :attachable, :dependent => :destroy
accepts_nested_attributes_for :assets

validate :validate_attachments
def validate_attachments
errors.add_to_base("Too many attachments - maximum is #{Asset::Max_Attachments}") if assets.length > Asset::Max_Attachments
assets.each {|a| errors.add_to_base("#{a.name} is over #{Asset::Max_Attachment_Size/1.megabyte}MB") if a.file_size > Asset::Max_Attachment_Size}
end
class AssetsController < ApplicationController
def show
asset = Asset.find(params[:id])
#do security check here
send_file asset.data.path, :type => asset.data_content_type
end

#this will be called via ajax/remote
def destroy
asset = Asset.find(params[:id])
@allowed = Asset::Max_Attachments - asset.attachable.assets.count

@attachable = asset.attachable

if asset.destroy
respond_to do |format|
format.html do
if request.xhr?
#get attachable item again to ensure we get the new asset list
render :partial => "attachments", :collection => Attachable.find(@attachable.id).assets
end
end
end
else
respond_to do |format|
format.html do
if request.xhr?
render :json => asset.errors
end
end
end
end
end
end

In the forms:

#the file field to add a new one. Disabled if there are already the max number of attachments
<div id="attachment_fields">
<% if @attachable.assets.count >= Asset::Max_Attachments %>
<input id="newfile_data" type="file" disabled />
<% else %>
<input id="newfile_data" type="file" />
<% end %>
</div>
#the list of pending attachments
<p>Pending Attachments: (Max of <%= Asset::Max_Attachments %> each under <%= Asset::Max_Attachment_Size/1.megabyte%>MB)</p>
<div id="attachment_list">
<ul id="pending_files"></ul>
</div>
#the list of already attached files (required for edit form only)
<p>Attached Files:</p>
<div id="attachment_list">
<%= render :partial => "attachment", :collection => @attachable.assets %>
</div>

The attachment partial:

<% if !attachment.id.nil? %>
<li id="attachment_<%=attachment.id %>">
<a href="<%=attachment.url %>">
<%=attachment.name %>
</a>
(<%=attachment.file_size/1.kilobyte %>KB)
#only needed if in edit action
<% if action_name == "edit" %>
<%= link_to "Remove", asset_path(:id => attachment), :method => :delete, :confirm => "Are you sure you want to delete " + attachment.name + "?", :remote => true, :class => 'remove_file' %>
<% end %>
</li>
<% end %>

Okay so that almost it. The last thing we need to add is the javascript to remove a Asset via ajax, handle the file field change event and also remove a non-uploaded file.

$(document).ready(function(){
//change form button text on submit. Handy for general usage.
$('form').submit(function(){
$('input[name="commit"]', this).attr('value', "Submitting...");
$('input[name="commit"]', this).attr('disabled', 'disabled');
});

//ajax for removing the file from the list of attachments
$('.remove_file')
.live("ajax:beforeSend", function(evt, xhr, settings){
var $form = $('.attachable_form');
var $submitButton = $form.find('input[name="commit"]');

$submitButton.data('origText', $submitButton.attr('value'));
$submitButton.attr('value', "Waiting...");
$submitButton.attr("disabled", true);
})
.live("ajax:success", function(evt, data, status, xhr){
$('#attached_list').html(xhr.responseText);
})
.live("ajax:complete", function(evt, xhr, status){
//always set form class to 'attachable_form' when they have an asset
var $form = $('.attachable_form');
var $submitButton = $form.find('input[name="commit"]');

$submitButton.attr('value', $submitButton.data('origText'));
$submitButton.attr("disabled", false);
})
.live("ajax:error", function(evt, xhr, status, error){
var $form = $('.attachable_form'),
errors,
errorText;

try {
// Populate errorText with the comment errors
errors = $.parseJSON(xhr.responseText);
} catch(err) {
// If the responseText is not valid JSON (like if a 500 exception was thrown), populate errors with a generic error message.
errors = {message: "Please reload the page and try again"};
}

// Build an unordered list from the list of errors
errorText = "There were errors with the submission: \n<ul>";

for ( error in errors ) {
errorText += "<li>" + error + ': ' + errors[error] + "</li> ";
}

errorText += "</ul>";

// Insert error list into form
$('div#errorExplanation').html(errorText);
});
$('input:file').live('change', function(){
var index = $('#pending_files').children().size();
var totalAssets = index + $('#attachment_list').children().size() + 1;
if (totalAssets < 5)
{
$('#attachment_fields').prepend("<input type='file' id='newfile_data' />");
}
else
{
$('#attachment_fields').prepend("<input type='file' id='newfile_data' disabled />");
}
$(this).css('position', 'absolute');
$(this).css('left', '-1000px');
$(this).attr('name', 'attachment[file_' + index + ']');
var fileText = "<li>" + $(this).val() + " <a href='#' class='remove_pending' title='Remove this attachment'>Remove</a></li>";
$('#pending_files').prepend(fileText);
});
$('.remove_pending').live('click', function(){
var thisIndex = $('#pending_files').index($(this).parent());
var position = thisIndex++;
$('#attachment_fields').children().eq(position).remove();
$(this).parent().remove();
var totalAssets = $('#pending_files').children().size() + $('#attachment_list').children().size();
if (totalAssets < 5)
{
$('#attachment_fields input:first-child').removeAttr('disabled');
}
return false;
});
});

To create this, I read a bunch of tutorials on the many different aspects of rails including the following:

I hope that this post has helped you out!

Thanks,

Cameron

  • Serg

    Hello! Thank you for good tutorial. I try to erproduce it, but I don’t understand, which form must contain “in forms” part. If I include in my model (e.g. Page) ‘has_many :assets, :as => :attachable, :dependent => :destroy’, and then include to page’s form this code, it does not work, because @attachable is nil. So I do @attachable=@page. It whoes, but does not add file….

    • Cameron

      The form that would contain the “in forms” part is either the new and/or edit form for the @page (assuming a page has attachments). Could you perhaps email me what you currently have in your form?