AJAX with acts_as_commentable
I had fun this weekend with AJAX-ifying an implementation of the acts_as_commentable plugin. After reading (and implementing) Dave Naffis’ article on adding AJAX to the acts_as_rateable plugin, I was inspired to do the same with comments.
Well, actually, I was inspired to google the problem to see if anybody else had done it. But imagine my glee when I found out that I could be the first to write about it!
Before I get into this, let me first qualify my skills. I am a complete newbie to RoR. There. That’s out of the way. Now let’s get going.
First of all, follow the links above to install the acts_as_commentable plugin, run your migration and add the acts_as_commentable line to your model. The links above explain it well so I don’t want to repeat their work. If you have any problems with this part then please leave a comment and I’ll help you out as best I can.
Next you need to add some code to your view. In my case, I was displaying an individual map to the user and they were able to make comments on it. Code needed to be added that would display existing comments as well as adding a form to allow for new comments. This is what I added to maps/show.rhtml:
-
<h5>Comments</h5>
-
<div id=“comments”>
-
<% @map.comments.each do |comment|%>
-
<%= render :partial => “comment/comment”, :locals => {:comment => comment } %>
-
<% end %>
-
</div>
-
<h5>Leave a comment</h5>
-
<% remoteformfor :comment, :url=>{:controller=>“comment”, :action=>“new”, :id=>@map.id } do |f| %>
-
Title: <%= f.textfield “title” %><br>
-
Comment:<br>
-
<%= f.textarea “comment”, :rows=>5, :cols=>35 %><br>
-
<%= submit_tag “Add Comment”%>
-
<% end %>
This code simply creates a div named comments that contains (oddly enough) all of the comments for a map. It’s important to use a named div here for the AJAX part that will be coming up. In addition to the named div, there is also a form that uses the AJAXy version of a form called remote_form_for. The remote form posts a comment to the new action of the comment controller. In addition to passing the comment information, @map.id is also passed to ensure that the new comment can be attached to the map that is currently being viewed.
OK…with the view covered, we’re off to see the controller. Here’s the code for comment_controller.rb:
-
class CommentController <ApplicationController
-
def new
-
@map = Map.find(params[:id])
-
-
@comment = Comment.new(params[:comment])
-
@comment.created_at = Time.now
-
@map.comments <<@comment
-
render :layout=>false
-
end
-
end
To start with, CommentController grabs the @map.id that was passed from the view. Then it creates a new comment, adds a timestamp and assigns the comment to the map. An implicit save is called here. Finally, it ensures that any layouts being used are not rendered. Because this is a remote form using AJAX, Javascript will be replacing the typical HTML return value so the layout isn’t needed.
In Rails, every action in a controller typically has a view. As we just stated, the view in this case isn’t HTML but is instead Javascript. That means that the view is named new.rjs rather than new.rhtml. Here’s the code:
-
page.inserthtml :bottom, ‘comments’, :partial => ‘comment/comment’, :locals => {:comment => @comment}
-
page.visualeffect :highlight, ‘comments’, :duration => 3
Pretty basic, eh? This simply adds a new comment to the bottom of the current list of comments and then applies a visual highlight so that the user knows something changed. If you look carefully, you’ll see that the second parameter in each method call is ‘comments’. This is referring to the named div that we talked about earlier.
One other thing to note is that the rjs file doesn’t render out the HTML for the new comment. Instead, it passes that job off to a partial. Through a bit of parsing and a lot of convention, it knows to look for that partial in views/comment/_comment.rhtml. That also happens to be the same partial that the comment list used to render itself in maps/show.rhtml.
That’s basically it. I hit a few stumbling blocks that I’ll talk about in future posts. One of them was the difference in variable names between comment and @comment. I also had a tough time figuring out how to debug something like this so I’ll look into some options in the future (I’ve heard that FireBug might be useful).
This is the most code that I’ve added to one post. Let me know if any of this makes sense. I’m expecting that I’ll have to make some edits to make the process more clear. In the meantime, give this a try. It’s easy!
Trackbacks and Pingbacks
Comments are closed.
what is the code for the comments partial?
@ivan - I didn't add any code for the comments partial because it's pretty self-explanatory. Here it is:
I know, I figured it out a few minutes later. But thanks for the timeagoin_words, that's neat.
Hey, this tutorial helped me a bunch, thanks a lot. I've got a question, though, something I didn't understand.
Is there a benefit to rendering the comments with a block, rather than a partial with a collection? I'm having trouble posting more than one comment in succession, could that be the trouble?
@micah - a partial with a collection should work just fine. I just tested it with the following code and it worked OK:
< %= render :partial => "comment/comment", :collection => @map.comments %>
Can you post the code that you're having problems with?
Mike,
Thanks for the good tip. I followed exactly the same steps you explained above. However, AJAX insert_html part doesn't work. It does saves the comments ok though. Any guess?
def new @query = Query.find( params[:id] ) @comment = Comment.new( params[:comment]) @comment.created_at = Time.now @query.comments false
end
If I put AJAX part in comment controller, then it works fine. But not when I put in new.rjs... hmm any guess?
def new ... render :update do |page| page.insert_html :bottom, 'comments', :partial=>'comment', :locals => {:comment => @comment} end
end
If I put AJAX part in comment controller, then it works fine. But not when I put in new.rjs... hmm any guess?
Your explanation of the actsascommentable was very useful and worked well. I noticed you mentioned using the actsasrateable, do you have some sample code (much like above) for how to implement it?
Regards, Marion
@Mike, thanks for the great tutorial. It's exactly what I was looking for.
@Jay (or mike): I'm having the exact same issue you're having. Any solution?
@Trey - I put the link for the actsasrateable article right at the very top of the article. Here it is again: http://www.naffis.com/blog/articles/2006/08/31/rails-ajax-star-rating-system
@Jay & @Conan - I'll take a look at this again. It's been a while since I played with this code and I'm expecting that there might have been some changes since Rails 2.x.
Speaking of that, what version of Rails are you using with this? If it's a newer version of Rails then you might need to name your file "new.js.rjs" to accommodate the new file naming templates.
@mike -- So, I'm kind of embarrassed about this, but the issue was that I hadn't the JavaScript include in the HTML header of the template that used this.
After I added "javascriptincludetag :defaults" it all worked dandy.
@conan - that's great to hear! Thanks for posting your solution back here.
And don't be embarassed - you have no idea how many times I've messed up as I put this post together. All of this trial and error adds up to experience.
I keep on getting the same error: ActionView::TemplateError (undefined local variable or method `comment' for #) on line #1 of comments/new.rjs: 1: page.inserthtml :bottom, 'comments', :partial => 'comments/comment', :locals => {:comment => comment} 2: page.visualeffect :highlight, 'comments', :duration => 3
Anyone else with this problem?
I was having same problem as you michael. For some reason the @comment variable was not being sent to rjs. I tried moving new.rjs into my controller, and that seemed to do the trick. My controller looks like this (replace video with whatever your actsascommentable is):
class CommentController 'comment/comment', :locals => {:comment => @comment } page.visual_effect :highlight, 'comments', :duration => 3 end } end end
end
Thanks for this! Your post saved me a boatload of time this evening.
Cheers Chris
This is awesome !!! Good work