Rails 4-State Ajax & CSS Star Rating

Dave Naffis just recently posted a how-to for Ruby on Rails Ajax & CSS Star Rating System on his blog. Funny enough, I implemented an almost identical equivalent just a few days earlier for another project with some minor differences. I also used Rogie's CSS Star Rating Part Deux and Chris Ingrassia's acts_as_rateable plugin in my version, but with one simple modification: I added a 'fourth' state to the rating model.

Rogie's CSS implementation allows us to have three states for each star: not filled, previously rated (current rating), currently highlighted. I wanted a fourth state, one that would distinguish between previously rated by others and previously rated by current user. In my application, once you rate an item, it stays that way and you can't re-rate it. My basic implementation is almost identical to Dave's so I will only outline the differences in my code:

<% @options = nparams.merge(:controller => "ctrl_name", :action => "rate", :id => @your_model) %>
<% @convert = %w[one two three four five] %>
   <ul class="star-rating">
    <% unless @model.rated_by_user?(current_user) %>
         <li class="current-rating" style="width:<%= @model.rating*12 %>px"> Currently <%= @your_model.rating %>/5 Stars.
     <% 1.upto(5) do |x| -%>
        <% rate_options = @options.merge(:rating => x) -%>
        <li><%= link_to_remote "#{x}",
        { :url => url_for( rate_options ), :loading => "Element.show('t-#{@model.id}-indicator')" },
        { :href => url_for( rate_options ), :class => "#{@convert[x-1]}-stars", :title => "#{pluralize(x,'star')} out of 5" } -%>
          <%end -%>
        <% else %>
          <li class="current-rating-rated" style="width:<%= @model.rating*12 %>px"> Currently <%= @model.rating %>/5 Stars.
        <% end %>

  </li></ul>

As you may have guessed, the magic happens in the if !@rated statement. In my controller, I check if the current user has already rated an item and output the corresponding partial. Plus, unlike Dafe's implementation I took care of my stars in a for loop to avoid duplicating same code. Now, all we need to do is add yet another single star image (just one star, same size!) and create a new CSS rule:

.star-rating li.current-rating-rated {
  background: url(/images/stars-rated.gif) left bottom;
  position: absolute;
  height: 12px;
  display: block;
  text-indent: -9000px;
  z-index: 1;
}

Make sure the height property is set to the same value as your other CSS stars and that is it! Now you have a 4-state AJAX/CSS star rating system. Gotta love it.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.