home icon contact icon rss icon

Tweaking Rails app with jQuery, part I

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:

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">&nbsp;</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. :)

Autotest and KNotify

I’ve configured KNotify to work with ZenTest. To see knotify messages just drop below code to ~/.autotest


module KDENotify
 def self.span str, color
   "<span style=\"color: #{color}\">#{str}</span>" 
 end

 def self.notify title, msg, color
   system "dcop knotify default notify " +
          "eventname '#{span(title, color)}' '#{span(msg, color)}' '' '' 16 2" 
   end

 Autotest.add_hook :ran_command do |at|
   if at.results.split("\n").last.first =~ /([0-9]+\sexamples,\s([0-9]+)\sfailures?(,\s([0-9]+) pending)?)/
     message, failures, pending = $1, $2.to_i, $4.to_i
     if failures > 0
       notify "Tests failed", message, "darkred" 
     elsif pending > 0
       notify "Tests passed with some tests pending", message, "goldenrod" 
     else
       notify "Tests passed", message, "darkgreen" 
     end
   end
 end
end

Upload progress script with safari support

Quick links:

Recently I’ve wrote about apache upload progress module. I work mainly on linux and I haven’t check my scripts on safari. It was working even on IE, what possibly could be harder to obtain? ;-) Some people reported that demo is not working on safari and Michele resolved the problem (thanks Michele :).

Solved! The only thing to do was to open WinXP on VirtualBox and check it on Safari 3. Michele’s solution worked well, but to make it work there must be html page with given structure and javascript. I like “one file” easy to use scripts without any issues with static files :) So what? Create an iframe dynamically, load scripts dynamically, one file, the only thing that user will have to set is path to scripts.

With Safari? No, not really. I’ve wrote it in a few minuttes and checked in firefox. It worked, great, now safari. Nope….

Although Safari have great CSS support, it is really terrible with Javascript. WYSIWYG, javascript history, ajax issues, now the upload progress and iframes. In edge case libraries I often see hacks for IE and safari mainly.

Luckily Apple released Windows version of safari and I can check my scripts and pages on safari. Good… developer tools don’t work, though… I had to do some alert-like IE style debugging ;-) After many hours of trying new more and more insane ways to create iframe and load scripts into it, code with document.write() worked!

Check the commit on github. Lines 18-22 especially. And 28-line issue with safari not waiting to load previous script.

It’s really sad that after working on firefox with firebug or opera with developer tools I have to fight with Safari which is supposed to be modern browser. I know that on Mac developing scripts for safari is easier, thanks to working debug tools, but hey! I work on Linux. Why do I have to run Safari on Wine or VirtualBox, lacking debug tools? It’s a pain. I feel like I’m working on explorer…

I will add prototype version and possibly some usage page shortly (for know look at the demo code).

Upload progress bar with mod_passenger and apache

UPDATE: I found 2 bugs in upload progress module. If you have already installed. update to at least 0.1 version: http://github.com/drogus/apache-upload-progress-module/commits/0.1

I’ve installed mod passenger on my server recently. It’s really great software. Now I don’t have to worry about monitoring, nginx proxy, load balancing, big file uploads… and it’s fast! With Ruby Enterprise Edition it’s even faster.

Personally I don’t care about people saying that phusion wants to promote themselves on REE as long as it gives faster ruby with lower memory use (but yes, I know, REE is not best choice for a name :).

After installing I’ve realised that my shiny upload progress bar (thanks to Upload Progress Module for nginx) was not working. Oh gods! What a tragedy!

But fear not. I’ve written apache upload progress module to have my lovely progress bar back again. As a lazy developer I’ve implemented reports in the same way as in nginx upload progress, so my applications are working without changing any signle line of code. If you were using nginx upload progress just drop the module, change your config and you’re good to go :)

I’m testing it in one of my production servers, but be carefull – it’s not well tested in other enviroments (i have gentoo with apache 2.2.8). Any feedback will be helpfull. Give me a note in comments if you encounter any problems.

So you want to be cool and have your own sexy progress bar in your app? Keep reading ;)

To install module you must download it using git:
git clone git://github.com/drogus/apache-upload-progress-module.git
or get the package: http://github.com/drogus/apache-upload-progress-module/tarball/master To compile/install/activate you have to use apxs2:
apxs2 -c -i -a mod_upload_progress.c
  • -c is for compiling
  • -i is for installing (copy mod_upload_progress.so to apache library dir)
  • -a is for activating (add LoadModule option into your apache conf file)
If you want to install and activate run this command as a root. Otherwise you can just compile and add LoadModule to apache conf:
LoadModule upload_progress_module path/to/apache-upload-progress-module/.libs/mod_upload_progress.so
Currently there is only one global option:
UploadProgressSharedMemorySize 1024000

This sets shared memory size to 1M. By default it’s 100kB.

To add tracking and reporting upload for a virtual host in apache you will need to add:

<Location />
    # enable tracking uploads in /
    TrackUploads On
</Location>

<Location /progress>
    # enable upload progress reports in /progress
    ReportUploads On
</Location>

Now all uploads will be tracked and reports are under /progress

Format of the report is JSON. From nginx wiki:

The returned document is a JSON text with the possible 4 results:
  • the upload request hasn’t been registered yet or is unknown:

new Object({ ‘state’ : ‘starting’ })

  • the upload request has ended:

new Object({ ‘state’ : ‘done’ })

  • the upload request generated an HTTP error:

new Object({ ‘state’ : ‘error’, ‘status’ : })

One error code that is interesting to track for clients is HTTP error 413 (Request entity too large)

  • the upload request is in progress:

new Object({ ‘state’ : ‘uploading’, ‘received’ : , ‘size’ : })

The HTTP request to this location must have either an X-Progress-ID parameter or X-Progress-ID HTTP header containing the unique identifier as specified in your upload/POST request to the relevant tracked zone. If you are using the X-Progress-ID as a query-string parameter, ensure it is the LAST argument in the URL.

Now the last thing to do is to implement progress bar. I don’t like repeating others and there is great tutorial on setting up upload progress bar with nginx and merb

UPDATE: I released jquery upload progress library with Safari 3 support. More info here. UPDATE2: I’ve upgraded prototype version to work in Safari.

It’s for merb and nginx but if you drop the scripts in your rails app and with apache-upload-progress-module it will work. :) Basically if you have your own code handling uploads (for example using attachment_fu) you can just add javascript and css – it’s unobtrusive.

If you’re using prototype I’ve rewritten script and made a demo. You can also grab files

I hope you enjoy this article. Progress bar is in my opinion one of the most useful technics – there is nothing more annoying than large file uploading without any info on state of an upload.