Star Rating for Rails 3 and jQuery

Sun, 28.11.2010 rating, rails 3, jquery, ajax

You might be starting a project (or upgrading an older one) on our shiny newly branded Rails 3, and in that process, you might as well be looking for a solution to the problem of offering a trendy Star Rating system in your app. Googling for “Rails rating” tells you to go with the ajaxful-rating plugin, to which I won’t offer you the link, since that won’t work (for now) with Rails 3.

There are Other Solutions TM out there, but some won’t work if you have more than one Star Rating Form in a single page (although much of this post was inspired by it). But there are good news: you needn’t reinvent the wheel. You are probably using jQuery anyways, so why not just use one of its plugins? jQuery Star Rating Plugin is the way to go. And why? Because it is great, and I am telling you here how to make use of it! (but be careful, I use it only in a prototype of an app that we are building, no production stories yet).

So, you installed the jquery-rails plugin (at least version 0.2.5 if downloading from github). You also downloaded the files from the jQuery Star Rating Plugin and put them into your public/javascripts folder, along with its (only!) two images under public/images and its stylesheet under public/stylesheets/jquery.rating.css . What’s next? Obviously, you need a set of models, controller and views. I went with the good ol’ acts_as_rateable plugin, also available as a gem, but you might not need so much power (it polymorphically allows to rate different models). Let’s say you want to rate comments on a post.

1
2
3
4
class Comment
  belongs_to :post
  acts_as_rateable
end

And under app/views/posts/show.html.erb you have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% for comment in @post.comments %>
  <h4><%= comment.title %></h4>
  <p><%= comment.body %></p>
  <p>by <%= comment.author %> on <%= comment.date %></p>
  <p>
    Rating: <%= comment.average_rating %>
    <%= form_for(Ratings.new) do |f| %>
      <% 1.upto(5) do |value| %>
        <%= star_button f, comment, value, (value==5) %>
      <% end %>
      <%= hidden_field_tag("comment_id", comment.id) %>
    <% end %>
  </p>
<% end %>

That’s a simple example of how comments would look like (you may extract everything inside the for into a partial, but that’s another story). Notice a couple of things:

  • There is no way to change your rating once you cast it. For what I know, acts_as_rateable doesn’t let you do it either, but I did not investigate much on that yet.
  • We are offering 5 stars, you might want other solutions for your upto(5) (at line 8).
  • There are two helper methods on that code:
    1. average_rating (at line 6) comes with acts_as rateable and gives us, well, eh…, yes, the average rating of the given comment.
    2. star_button (at line 9) I wrote:

      1
      2
      3
      
        def star_button(f, comment, value, checked)
          radio_button_tag("rating[#{comment.id}]", value, checked, :class => 'star')
        end
      

      This key method is self explaining, and builds the markup that our jQuery Star Rating Plugin understands. I would only point out that you could make the checked radiobutton/star appear a value different than my chosen 5, for instance referring to the last rating score given by the current_user or to the mean of users (average_rating) or something else. I chose it quite neutral.
      Put it inside app/helpers/posts_helper.rb , and that will provide the following code:

      1
      2
      3
      4
      5
      
      <input class="star" id="rating_15_1" name="rating[15]" type="radio" value="1" />
      <input class="star" id="rating_15_2" name="rating[15]" type="radio" value="2" />
      <input class="star" id="rating_15_3" name="rating[15]" type="radio" value="3" />
      <input class="star" id="rating_15_4" name="rating[15]" type="radio" value="4" />
      <input checked="checked" class="star" id="rating_15_5" name="rating[15]" type="radio" value="5" />
      

      where 15 is the comment’s id. Respect the class star for the plugin to work.


You can already let it run and see how it looks like. Not that bad for that little work. And the best news is: we are almost finished. Get this at app/controllers/ratings_controller.rb :

1
2
3
4
5
6
7
8
9
10
11
class RatingsController < ApplicationController
  def create
    @comment = Comment.find_by_id(params[:comment_id])
    if @comment.rate_it(params[:rating][@comment.id.to_s], current_user.id)
      respond_to do |format|
        format.html { redirect_to post_path(@comment.post) :notice => "Your rating has been saved" }
        format.js
      end
    end
  end
end

The method rate_it (at line 4) also comes with acts_as_rateable , and wants a value (coming from the POST params) and a user rating the comment.
Notice that we are trying to allow nice degradation when JS is not activated. That’s why we provide format.html (at line 6). You can try this as a pure HTML form (add a submit button on your form!, like this: <%= f.submit :rate %>).
But for AJAX to work, we need a last thing. The one that motivates this post, since the jQuery Star Rating Plugin doesn’t provide a way to save your rating.
Put the following under public/javascripts/jquery.rating.save.js :

1
2
3
4
5
6
7
8
9
10
$(document).ready(function() {
  // Hides the submit button
  $('.new_rating').children('input[type=submit]').addClass('hide');

  // Submits the form (saves data) after user makes a change.
  $('.star').click(function(){
        form = $(this).parent().parent();
        form[0].submit();
    });
});

Provided that you have a CSS class in your stylesheets called hide saying display:none , your submit button will disappear. And when you click on that rating star, it will automatically fetch its parent form and submit it. You can even have as many simultaneous forms in your page as you like.

Let me know if you get some issues. Happy rating!

14 comments