Skip to content

28 Creating Threaded Comments

Dave Strus edited this page Jul 16, 2015 · 1 revision

It's time for threaded comments. In the words of Sir Martin of Lawrence, "[Poop] just got real."

We'll start with creating threaded comments.

We'll update CommentsController to assign parent_id to new comments from params. We do not need to permit this parameter, because it won't be mass-assigned.

app/controllers/comments_controller.rb

    comment = Comment.build_from post, current_user.id, comment_params[:body]
    comment.parent_id = params[:comment][:parent_id]

Let's extract the comment form from posts/show into a partial. In the process, we'll need to change the instance variable @comment to a local variable. We'll also add a hidden field for parent_id.

New directory: app/views/comments

New file: _app/views/comments/form.html.erb

<%= form_for comment, html: { class: 'comment' } do |f| %>
  <fieldset>
    <%= f.label :body, 'Add a comment' %><br />
    <%= f.text_area :body %>
  </fieldset>
  <%= f.hidden_field :commentable_type %>
  <%= f.hidden_field :commentable_id %>
  <%= f.hidden_field :parent_id %>
  <%= f.submit class: 'btn btn-default' %>
<% end %>

We'll render the partial in place of the old form in posts/show.

app/views/posts/show.html.erb

  <%= render 'comments/form', comment: @comment %>

At this point, we shouldn't have broken anything at least. If everything works as before, we're ready to continue. In fact, let's make an incremental commit.

$ git add .
$ git commit -m "Extract new comment form into a partial."

For replies to specific comments—child comments, in other words—we want to load that form partial via Ajax. Let's add an element for each comment for the form to be loaded into.

app/views/posts/show.html.erb

        <section class="body">
          <%= comment.body %>
        </section>

        <div id="reply-form-<%= comment.id %>" style="display: none">
        </div>

Create a comments/new JavaScript view, and corresponding route.

config/routes.rb

  resources :comments, only: [:create, :new]

New file: app/views/comments/new.js.erb

Notice the file extension of our new view is .js.erb rather than .html.erb. It still uses embedded Ruby, but it will be sent to the browser as JavaScript.

Now add a new method to CommentsController. In order for our hidden fields to show up with the correct values, we'll need to get the parent_id from params, load the parent comment, load the associated post, and assign all of those values to the new comment.

Note that there will be no HTML view for this action. Only JavaScript. We'll include a respond_to block accordingly.

app/controllers/comments_controller.rb

  def new
    @parent = Comment.find params[:parent_id]
    post = @parent.commentable
    @comment = Comment.build_from(post, current_user.id, '')
    @comment.parent_id = @parent.id
    respond_to do |format|
      format.js {}
    end
  end

Now for our JavaScript view. The JavaScript code contained in this view will be executed in the browser after the Ajax request is finised. We want to do a few things here. First, we want to replace the contents of the appropriate reply-form-xx div with the contents of the form partial. We'll use jQuery to accomplish this.

app/views/comments/new.js.erb

$("#<%= "reply-form-#{@parent.id}" %>").html("<%= escape_javascript(render 'form', comment: @comment, parent_id: @parent.id) %>");

We have two pieces of embedded Ruby on this line. The first inserts the ID of the parent comment in order to correctly identify the <div> into which the partial will be loaded. The second is where the partial is actually rendered. In the second case, we'll need to use the escape_javascript helper so the JS doesn't get tripped up on double-quotes and the like.

We'll put two more lines in the JS view. The first will make the reply form appear via a slide-down effect. The second will focus the <textarea> on the form.

app/views/comments/new.js.erb

$("#<%= "reply-form-#{@parent.id}" %>").slideDown(350);
$("#<%= "reply-form-#{@parent.id}" %> textarea").focus()

Now we need an Ajax link to load all of that business. We'll add this "reply" link in <ul class="actions">, just as we do the action links for the posts themselves. We'll put it right after the comment body.

app/views/posts/show.html.erb

        <section class="body">
          <%= comment.body %>
        </section>
        <ul class="actions">
          <li><%= link_to 'reply', new_comment_path(parent_id: comment.id, format: 'js'), remote: true %></li>
        </ul>
        <div id="reply-form-<%= comment.id %>" style="display: none">
        </div>

Let's take a closer look at the link itself.

link_to 'reply', new_comment_path(parent_id: comment.id, format: 'js'), remote: true

We are linking to new_comment_path and specifying the js format, as we'll expect a JavaScript response. We're also passing the ID of the parent comment in a query string param, as CommentsController expects.

The remote: true option in link_to specifies that this is an Ajax link. Rails will take care of the JavaScript necessary to make the Ajax request. Nifty, huh?

Give it a shot to see if the forms work. Mind you, the comment threads are still being flattened when they're displayed, so you won't notice that the nesting worked. You'll have to check that out in the console for the moment.

If it fits, commits!

$ git add .
$ git commit -m "Create threaded comments."