Quick links:
- completed demo (demo is reseted every few hours)
- sources of application on github (or download tarball)
I’m in the train from Zgorzelec to Warsaw returning from my girlfriend’s place. Polish trains are like turtles, so I will have pretty much time for writing ;-)
I’ve wrote (or maybe it’s better to say copy&paste) little rails app like in Mike Clark’s tutorial for attachment_fu. A few months ago there was Mugshots exhibition in Yours Gallery in Warsaw based on work of Peter Doyle. I saw it with Kathleene, she took some pictures. Great! I have material to fill my new app, what else could I possibly dream of?! (yeah… macbook, but it’s obvious ;-).
Now you can admire my hard work: mugshots.drogomir.com/js/no-javascript/mugshots/
But wait… It’s not so cool… where are all those shiny javascript effects? Don’t worry. I will show you how to spice this dish.
We will need:- jQuery
- jQuery form plugin
- jQuery livequery plugin
- jQuery upload progress
- jquery blockUI
- jquery mutli file
- jquery lightBoxFu
- lightbox
I’ve pushed application to github, so you can see entire code. Clone it or grab the tarball
There is one thing that is not straight forward. @main_js variable in app/views/layouts/main.rhtml:
<%= javascript_include_tag @main_js %>
It’s there for changing javascript file loaded. When url is app.com/js/some_javascript_file/mugshots, @main_js should be “some_javascript_file.js”. I’ve done this to have possibility to show you app with different javascript files without changing the code. See routes and mugshots_controller.rb to find out how it was done (or run “rake routes” in app dir to see routes).
Lets begin.
What to do first? It’s all about uploading files, so I would add upload progress bar to form in mugshots.drogomir.com/mugshots/new. To implement it you will need some kind of server module:
You have to install and enable one of the above modules to make progress bar work.
Then add some javascript to applications.js. This example is using “LightBoxFu”: – little script that I wrote to show progress bar as an overlay. It’s based on Riddle’s work – all positioning is in CSS (except expressions for IE) so it’s really light and fast. Ideal for such a task. If you don’t like lightBoxFu you can use any other form of displaying message (you can use some other lightbox with displaying code function or even blockUI plugin).
// handy trick, if we can't use $ beaceuse jQuery.noConflict
// was used, jQuery is passed as argument in document ready
// so we can name it $
jQuery(function($) {
// add upload progress to our form
$('form.progress').uploadProgress({
start:function(){
// after starting upload open lightBoxFu with our bar as html
$.lightBoxFu.open({
html: '<div id="uploading"><span id="received"></span><span id="size"></span><br/><div id="progress" class="bar"><div id="progressbar"> </div></div><span id="percent"></span></div>',
width: "250px",
closeOnClick: false
});
jQuery('#received').html("Upload starting.");
jQuery('#percent').html("0%");
},
uploading: function(upload) {
// update upload info on each /progress response
jQuery('#received').html("Uploading: "+parseInt(upload.received/1024)+"/");
jQuery('#size').html(parseInt(upload.size/1024)+" kB");
jQuery('#percent').html(upload.percents+"%");
},
interval: 2000,
/* if we are using images it's good to preload them, safari has problems with
downloading anything after hitting submit button. these are images for lightBoxFu
and progress bar */
preloadImages: ["/images/overlay.png", "/images/ajax-loader.gif"]
});
});
And some styling for progress bar:
#progress {
margin: 8px;
width: 220px;
height: 19px;
}
#progressbar {
background: url('/images/ajax-loader.gif') no-repeat;
width: 0px;
height: 19px;
}
That’s it, just add “progress” class to your form and progress bar is working:
<% form_for(:mugshot, :url => mugshots_path,
:html => { :multipart => true, :class => "progress" }) do |f| -%>
Uploading files looks much better right now, check it here: http://mugshots.drogomir.com/js/progress/mugshots/new
So what now? I find the “add photo, click New mugshot, add photo” scenerio annoying. We could add more than one file on each submit. For that we will use jquery.MultiFile.js. This one is a bit tricky, cause we will need to tweak code handling uploads also.
Javascript enabling mutlifile:
jQuery(function($) {
$('.multi-file').each(function() {
// change name of element before applying MultiFile
// so array of files can be send to server with mugshot[uploaded_data][]
$(this).attr('name', $(this).attr('name') + '[]');
}).MultiFile();
});
We must also add “multi-file” class to file field:
<%= f.file_field :uploaded_data, :class => 'multi-file' %>
From javascript point of view that’s all. Let’s see how uploaded photos are handled by rails app:
@mugshot = Mugshot.new(params[:mugshot])
So mugshot.uploaded_data is filled with data from params[:mugshot][:uploaded_data]. Good for one file. But with array of files we should create Mugshot for each file. I would add a method in model:
def self.handle_upload(mugshot_params)
# array for not saved mugshots
mugshots = []
if mugshot_params[:uploaded_data].kind_of?(Array)
mugshot_params[:uploaded_data].each do |p|
unless p.blank?
mugshot = Mugshot.new(:uploaded_data => p)
mugshots << mugshot unless mugshot.save
end
end
else
mugshot = Mugshot.new(mugshot_params)
mugshots << mugshot unless mugshot.save
end
mugshots
end
and slightly change controller code:
def create
@mugshots = Mugshot.handle_upload(params[:mugshot])
# if @mugshots is empty there are no errors
if @mugshots.blank?
flash[:notice] = 'Mugshot was successfully created.'
redirect_to mugshots_url
else
render :action => :new
end
end
Only one problem left. Validation.
Easiest way is to change error_messages_for:
<%= error_messages_for :object => @mugshots %>
It works. But suppose you are uploading 3 files and 2 of them are too big. You will end with:
- Size is not included in the list
- Size is not included in the list
Which one was added? Some lottery here…
I would tweak attachment_fu error messages a bit. By default it uses validates_as_attachment method which simply adds:
validates_presence_of :size, :content_type, :filename
validate :attachment_attributes_valid?
Instead validates_as_attachment we can isert our new code:
validates_presence_of :size, :content_type, :filename, :message => Proc.new { |mugshot| "can't be blank (#{mugshot.filename})" }
validate :attachment_attributes_valid?
def attachment_attributes_valid?
[:size, :content_type].each do |attr_name|
enum = attachment_options[attr_name]
errors.add attr_name, "#{ActiveRecord::Errors.default_error_messages[:inclusion]} (#{self.filename})" unless enum.nil? || enum.include?(send(attr_name))
end
end
Now it’s a lot more readable:
- Size is not included in the list (filename.jpg)
- Size is not included in the list (filename1.jpg)
Submit form looks better now, but viewing files is still ugly. Maybe we could add some lightbox? No problem:
$('#mugshots li a').lightBox();
I used that lightbox cause I had it configured for my previous rails apps, but pick your favourite one, as there are gazilions of them.
This is first step of tweaking our app. Javascript is in step1.js file: mugshots.drogomir.com/js/step1/mugshots/new
What now? User can upload many files at one submit and see progress bar. What else do we need? Ajax! My preciousssss…
As all children know, XMLHttpRequest can’t upload files. What a shame… our new tweaked mugshots app is all about uploading files. Although you can’t do it with XHR, there is a way to imitate it. It is obtained by creating an iframe and uploading files to it.
Luckily Mike Malsup has done hard work for us writing jQuery form plugin.
First, we need our form. I would place it instead “New mugshot” link. Link has id=”new_mugshot_link”, so this piece of code will replace it with form:
/* create upload form with multifile instead of new mugshot link */
var form = $('<form method="post" enctype="multipart/form-data" class="progress ajax" action="/mugshots">');
var label = $('<p><label for="mugshot_uploaded_data">Upload mugshot: </label></p>');
var input = $('<input type="file" class="multi-file" id="mugshot_uploaded_data" size="30" name="mugshot[uploaded_data]"/>');
label.append(input).appendTo(form);
form.append('<p><input type="submit" value="Create" name="commit"/></p>');
if (typeof(AUTH_TOKEN) != "undefined") form.append('<input type="hidden" value="'+AUTH_TOKEN+'" name="authenticity_token"/>');
$('#new_mugshot_link').replaceWith(form);
Our form has to be send to an iframe, so we have to apply ajaxForm to it. After replacing link with form we can’t figure out when form is actually appended to DOM. To be sure that form is there, we can use livequery. It will fire callback function when ‘form.ajax’ will be available:
$('form.ajax').livequery(function() {
$(this).ajaxForm({iframe: true, success: function (responseText, statusText, form) {
var url = $(form).attr('action');
/* get new files */
$.ajax({
url: url,
dataType: "script",
beforeSend: function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");},
/* we need to update lightbox array to include new files */
complete: function() { $('#mugshots li a').lightBox(); }
});
}});
});
When new form tag with class “ajax” will be available callback function will be run. iframe option tells form plugin to add hidden iframe (it will handle file upload).
The above code has ajax call to ”/mugshots” url which will run index.js.erb (RJS), so we will need one:
app/views/mugshots/index.js.erb
jQuery('#mugshots').html(<%= js render(:partial => 'mugshot', :collection => @mugshots) %>);
to handle it we need to use respond_to:
respond_to do |format|
format.html
# layout => false is here beaceuse without it rails are looking
# for layouts/index.js.erb
format.js { render :layout => false }
end
Normally I try not to use RJS to keep all my javascript (and ajax) logic in javascript files, but in case of images it isn’t so esay. I will write about it and about javascript templating systems in one of the next posts.
Take a look at: mugshots.drogomir.com/js/step2/mugshots Doesn’t it look nice?
There is only one problem :) No ajax validation. After submitting files, javascript can’t get any info about errors or uploaded files beaceuse it is treated like normal html request and response is loaded in an iframe. How to fix it? I’ll write about it in the next post. :)

David said
Jul 07, 2008 @ 03:26 AM
Thank you. This is great. I’m looking forward to part II. Hopefully, the Rails masses start moving towards jQuery.
Geoffroy said
Aug 29, 2008 @ 11:16 AM
Nice writeup. Thanks a lot for the clear explanation.
BTW, the photos are great, but kind of scary at the same time…
Kalle said
Oct 26, 2008 @ 03:28 PM
I have set this up to my Leopard with Apache upload progress module, but the upload doesn’t work. Progress thickbox opens but nothing happens, only text “Upload starting” and 0%. Nothing in Apache error log either.
Browser is Firefox 3. Any suggestions?
Kalle said
Oct 26, 2008 @ 03:43 PM
I used this to install upload progress module:
http://drogomir.com/blog/2008/6/18/upload-progress-bar-with-mod_passenger-and-apache#comment-465
Sachin Sagar said
Mar 26, 2009 @ 12:28 PM
I liked the implementation of jquery. Just eager to know when will its part – II will be released which might solve that only existing problem of ajax validation so that I could use this in production? Keep up the good posts.
RSS feed for comments on this post
Leave a Comment