The Accelerate HR Blog

Pop-up comment boxes in your Rails blog   (Thu Nov 29 2007)

I wonder if you noticed the little change I made to the Add a comment button on this blog yesterday. I'm not going to explain it. Just go to the end of this post and try it. You'll see.

Today I want to explain why and how I made the change.

If you're new here, comments weren't always thus. In the early days of Accelerate's design, I decided that it would be best to require people to log in to the site if they wanted to respond to posts. That way, I thought, I could minimize spam.

I need hardly have worried. I'm not setting any records for getting comments, let alone spam. Building a readership requires patience and hard work, I'm told. But the lack of comments got me thinking. Was it because my posts were so uncommentably boring? Perhaps. But when I fired up my trusty site visitor toolkit, I saw another possible reason. People who'd read the blog were sometimes going to the sign-up page, presumably to log in. And they were stopping right there.

Looking at it now, the reason's pretty obvious. On the sign-up page, we need 11 different responses. And who wants to answer 11 questions just in order to write a 2-word comment? With so many other goodies to explore on the Web today, time is precious.

So could I shorten the sign-up phase? No. As well as the sign-up for the blog, it's also the way you get access to the entire Accelerate HR database. All the sign-up questions are essential for the new database user - and if you're signing up for one of your key business tools, 11 questions won't seem too many.

So I needed a different way for my blog-only users to identify themselves. That was complicated a little further by the fact that for those who log in as Accelerate users, I'd offered the capability to review and edit their previous posts. And I'd done that by filling the :created_by field in my articles table with the registered user's unique login. So then in a your_comment controller I could find everything submitted by a single user:

def list
@user = session[:user]
@articlecomments = Articlecomment.find( :all,
:conditions => [ "created_by = ?", @user.login],
:order => "created_on DESC")
end

If I was going to allow the blog readers to enter their name without authentication or validation to prevent duplicates, then that was quickly going to screw this method. So change the signup and login system then? Not necessary. Thanks to the ease with which Rails migrations allow you to update your database schema, there was a much easier way. All I needed was this little migrant:

class AddCommentMarker < ActiveRecord::Migration
def self.up
add_column :articlecomments, :logged_in, :boolean, { :default => false }
end

def self.do
remove_column :articlecomments, :logged_in
end
end

So now, as a new comment is entered I can check to see whether there's a session[:user]. If so, the logged_in marker is set to true and I can grab the user's login. If not the marker is off and the user is named 'Guest'. (If you're interested in the code, I'll come to that later). But if the non-registered blog user wants to identify himself as IdaLupino and we already have a registered IdaLupino with access to the database, it's no longer a problem. Because if the real registered IdaLupino wants to see only his own comments, we've changed the list action in your_comment to:

def list
@user = session[:user]
@articlecomments = Articlecomment.find( :all,
:conditions => [ "created_by = ? and logged_in = ?", @user.login, true],
:order => "created_on DESC")
end

First problem solved. Now the next issue was how to bring the comment-box right onto the blog-page? Previously, we'd sent people to a new page - once again, we were likely to lose people who wanted to make a quick comment in a hurry. This is where is begins to get interesting from the Ajax/RJS/Scriptaculous point of view.


From here onwards, I'm referring to the page where all the most recent posts are listed - not to the page where we list a single post. If you're not there already, you might want to click on Accelerate HR Blog in the left-hand menu so that you can follow along.



I didn't just want to plonk a text-area directly on the page. As you can see, the page is already long. Far better, I thought, to click on Add a comment and get a cute little pop-up box.

This is how the view originally looked (omitting the styling, breaks, lines etc.). See if you can spot the terrible Rails crime I've committed.

<% for article in @articles %>
<h1> <%=h article.title %>
(<%=h article.created_on.strftime("%a %b %d %Y") %>)
</h1>

<p><%= article.content %></p>
<h4>Filed under: <%= article.tag_list %></h4>

<div id = "formend">
<%= link_to "Add a comment", :controller => 'your_comment',
:action => 'new',
:id => article %>
</div>

<% @articlecomments = article.related_comments %>
<% @articlecomments.each do | comment | %>
<div class = "comment">
Posted <%=h time_ago_in_words(comment.created_on) %>
ago by <b><%=h comment.created_by %></b>
<br /><br />
<%=h comment.comment %>
</div>
<% end %>
<% end %>

So we have a series of articles, each with a title, date-stamp, content and filing-tags. Then 'Add a comment' takes you out to a new page. Beneath that we list recent comments related to the article. (This is where the crime gets committed - see it now?) And each comment also carries the name of the contributor and a date-stamp.

Let's pause at the scene of the crime. In a view, you never, ever define your instance variables, right? That's done in the controller, or better still in the model. But that's exactly what I've done here, with @articlecomments. Why? Because I can't see a better way - if you can, please let me know. The problem is that the comments block is nested inside the article block. I can't see how we can define which comments we need to collect until we have selected the article to which they belong. And that doesn't happen in either the model or the controller, but in the view. Not good Rails practice, for sure, but at least it works ... until someone can suggest anything better.

Now let's look at how we modify the view to get our pop-up. First we need to create a named but empty div in the place where the pop-up is going to appear - that's just below the Add a comment link. Like this:

<div id = "add">
</div>

Then we need to change link_to to link_to_remote, because we're going to be using an RJS Template - enter stage-left Ajax, accompanied by his henchman, Curly Brackets. Here's my new line:

<%=link_to_remote("Add a comment", :url => {:action => :comment, :id => article}) %>

No call to an external controller any longer, you notice, because with Ajax, we're all on the same page. The comment action is now in the article controller. Before we look at the comment action in detail though, I want to jump ahead to the RJS template of the same name, because here I had a tricky problem - one I probably wouldn't have been able to solve without the assistance of Joe Hewitt's great Firebug tool (and another good reason to make Firefox your browser of choice).

RJS templates are usually really simple, making the power of Ajax available even to yokel programmers like me. All I needed to do was to issue an instruction to replace the contents of the "add" div (it's empty, remember?) with the contents of another partial. comment.rjs could be as simple as this:

page[:add].replace_html, :partial => 'my_comment'

The my_comment partial needs to contain the actual comment, the contributor's name, a confirmation button, and - importantly - a means to close the pop-up box without saving a comment. This last requirement means that we'll need a second RJS template. But this is all pretty straightforward stuff, so I won't bore you with the code here. Let's press on instead with the problem I met.

The page we're dealing with contains a loop - with the ten most recent articles. That means that our "add" div is going to appear 10 times on the page. And this is going to give comment.rjs in its current form a problem. I went to the fourth post and clicked on Add a comment. Sure a pop-up box appeared. But under post 1.

So we need to create a sequence of labels for our "add" divs - a different div for each article. And the RJS template needs to know which of those divs it is addressing. The sensible thing to do would be to add the article.id to the div label. So in the view the div becomes:

<div id = "add_<%= article.id %>">
</div>

But how to change the symbol [:add] in the RJS template. I don't know. But what I do know is that there's an alternative syntax for replace_html, and using that, I could do the necessary:

page.replace_html 'add_' + @id.to_s, :partial => 'my_comment'
page.visual_effect :blind_down, 'add_' + @id.to_s

Oh yes, there's a second line too. One of those fancy Scriptaculous effects. I tried Squidge and Fold and Shrink but in the end plumped for the staid and respectable Blind_down. (And Blind_up in the RJS template to close the pop-up box without saving.)

Just one more thing. The RJS template gets its instructions - in this case its @id instance - from the controller, so we need to make an adjustment there too. So here's the low-down on the comment action in the article controller:

def comment
#params[:id] is the article.id sent from the view
@id = params[ :id]
@user = session[ :user]
if @user.nil?
@contributor = "Guest"
else
@contributor = @user.login
end
@article = Article.find(params[ :id])
@articlecomment = Articlecomment.new
@articlecomment.article_id = @article.id
@articlecomment.created_by = @contributor
@articlecomment.logged_in = true unless @user.nil?
end

That's not really the end of the story. There's much more I could add on 'progressive enhancement' for Ajax - or 'graceful degradation' - basically the way we need to make sure that people who don't have Javascript switched on aren't left high and dry by our fancy Ajax techniques. As it stands, with the code I've given you here, they will be. But that's for next time, not today. Right now, I've got a database in the oven.

Just one word of warning if you're trying all this out. To get RJS templates working - and Scriptaculous for that matter, you need to make sure you include this statement in the <head> of your page or layout template:

<%= javascript_include_tag :defaults %>

So now you know all the why's and hows, why not try it out, and make that comment!

Filed under: Ruby on Rails






List recent Entries
List all blog entries filed under:

Employment Politics
HR
Implementation
Just thoughts
Ruby on Rails
Web 2.0

Can't find what you're looking for? Try this: -

Search blog for a word or phrase



 Subscribe to an RSS feed

Or get an email copy every time we post something new. Nothing new? Nothing mailed

Enter your email address:

Delivered by FeedBurner


If you're enjoying our blog, why not find out more, and maybe get involved?

ACCELERATE HR is a website built on Rails and designed for the enterprise. And we're building it live on the Web, right here.

Check out our home page HERE, or sign up for free HERE.


DESERT ISLAND BLOGS

Sharing a few of my favorites

HR Stimuli
McArthur's Rant

Jon Ingham's Strategic Human Capital Management Blog

The Rails Track
Railscasts

Web Power
The Technology Edge

Window on the Gulf
Mahmood's Den