Wednesday, October 3, 2007

5 Minute Ajax Voting in Rails

I (heart) Ruby and I like Rails. I don't love Rails, mind you, but some days I like it more than others. Today, I like it very much. Mostly because it was trivial to add nice Ajax-y voting behavior to my model with relatively little work. How little? It took less time to actually do, than it took me to write about it here.

So, here's how it's done: Install acts_as_voteable plugin as directed on Juixe. Sticking with their nomenclature, I will assume the active record you wish to treat as voteable is Post.
class Post < ActiveRecord::Base
acts_as_voteable
end
Quick Note: don't forget to add
<%= javascript_include_tag :defaults %>
to your html head, else you won't get all this delicious Ajaxiness.

Create a partial file called _vote.rhtml. This contains the snippet of code that actually generates the voting interface, as well as what is displayed to the user when it's updated.
<span id="votes<%= voteable.id %>">
<%= link_to_remote "+(#{voteable.votes_for})",
:update=>"vote",
:url => { :action=>"vote",
:id=>voteable.id,
:vote=>"for"} %>
/
<%= link_to_remote "-(#{voteable.votes_against})",
:update=>"vote",
:url => { :action=>"vote",
:id=>voteable.id,
:vote=>"against"} %>
</span>
Notice that we are suffixing the span's ID with the voteable object's ID. This is so you can render the _vote partial multiple times on a page, creating a unique block for each voteable object (for example, multiple Posts per page). The object is also named "voteable" and not "post" - this is because there is nothing about this file that is specific to Post, any object that acts_as_voteable can be passed into it.

When the above code renders, it will look like the following: +(5) / -(6) (yet, functional)

Next, create a file named vote.rjs that contains the Ajaxy goodness that dictates what to replace the span ID of "votesID". What does it replace it with? The exact same partial we just created. Only this time, the re-rendered version will have an updated vote-count.
page.replace("votes#{@post.id}", :partial=>"vote", :locals=>{:voteable=>@post})
To use the above code, just render the partial into your view (whatever it's call, for example, view_post.rhtml). Render the above partial and pass in the voteable object, as shown:
<%= render(:partial=>"vote", :locals=>{:voteable=>@post}) %>
That's all for the view layer - just two new files and adding a line of code to your existing view. Finally, to make the code work, add the vote action to your controller:
def vote
@post = Post.find(params[:id])
@vote = Vote.new(:vote => params[:vote] == "for")
@post.votes << @vote
end
This technically works. However, if you launch this, there will be no way to stop someone from clicking the link as desired, to get the score they want. The following extension will only allow logged-in users to vote, and then only once (made to work with acts_as_authenticated).
# One man, one vote
def vote
return unless logged_in?
@post = Post.find(params[:id])
unless @post.voted_by_user?(current_user)
@vote = Vote.new(:vote => params[:vote] == "for")
@vote.user_id = current_user.id
@post.votes << @vote
end
end
Note that I do not set @vote.user = current_user, this is to bypass an intermittent type error - I don't precisely know why it occurs, but is likely due to session object marshaling or gnomes. This is how I got SnipSnipe voting to work. More on its architecture after launch, stay tuned.

1 comment:

praethorian said...

Hi,

thx for really useful article, but I've got a warning while I am testing it.

"../vendor/plugins/acts_as_voteable/lib/acts_as_voteable.rb:42: warning: Object#type is deprecated; use Object#class" do you know, how to get rid of it?

Thx Pete