Using acts_as_commentable_with_threading Gem

When I was researching the acts_as_commentable gem I came across the spin off of it that has threading. I looked around for some examples of how to set it up, but just found scattered information.

I have set up a working example of the gem, acts_as_commentable_with_threading. You can find the repo on github.

Here is a look at what we are building.

Setting it up you will first add the gem to your Gemfile.

gem 'acts_as_commentable_with_threading'  

Then run, bundle install

Next you will want to run the migrations to set up your polymorphic Comments model.

rails generate acts_as_commentable_with_threading_migration  

Run rake db:migrate to generate your comment model.

Now we are ready to set up our models to use comments.

I have a Beer model and I want to add comments to them. In the model you will add this:

class Beer < ActiveRecord::Base  
  acts_as_commentable
end  

We need to add a controllers/comments_controller.rb that will have a create action.

class CommentsController < ApplicationController  
  before_action :authenticate_user!

  def create
    commentable = commentable_type.constantize.find(commentable_id)
    @comment = Comment.build_from(commentable, current_user.id, body)

    respond_to do |format|
      if @comment.save
        make_child_comment
        format.html  { redirect_to(:back, :notice => 'Comment was successfully added.') }
      else
        format.html  { render :action => "new" }
      end
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:body, :commentable_id, :commentable_type, :comment_id)
  end

  def commentable_type
    comment_params[:commentable_type]
  end

  def commentable_id
    comment_params[:commentable_id]
  end

  def comment_id
    comment_params[:comment_id]
  end

  def body
    comment_params[:body]
  end

  def make_child_comment
    return "" if comment_id.blank?

    parent_comment = Comment.find comment_id
    @comment.move_to_child_of(parent_comment)
  end

end  

Now over on beers_controller I will add a new_comment to the show action for a beer.

  def show
    @beer           = Beer.find(params[:id])
    @new_comment    = Comment.build_from(@beer, current_user.id, "")
  end

This will create a new commentable object that we can use for the comment form on the show.html.erb. First let's add a partial to this page. The way this is set up allows for it to be easily reused on any page that needs comments.

<%= render partial: "comments/template", locals: {commentable: @beer, new_comment: @comment} %>  

Here is the comments/_template.html.erb file:

<div class="comments-header">  
  <h4>
    <%= commentable.comment_threads.count == 0 ? "no" : commentable.comment_threads.count %> comments
  </h4>
</div>

<%= render partial: "comments/form", locals: {new_comment: new_comment} %>

<div class="comments-container">  
  <%= render partial: "comments/reply", locals: {comments: commentable.root_comments} %>
</div>  

This template itself is also built from other partials to keep the code clean.

First up is the comments/_form.html.erb at the top of the comments section.

<%= form_for @new_comment do |f| %>  
  <%= f.hidden_field :commentable_id, value: @new_comment.commentable_id %>
  <%= f.hidden_field :commentable_type, value: @new_comment.commentable_type %>
  <div class="field form-group">
    <%= f.text_area :body, class: 'form-control' %>
  </div>
  <div class="field form-group">
    <%= submit_tag "Post comment", class: 'btn btn-primary' %>
  </div>
<% end %>  

Followed up by the comments/_reply.html.erb - this displays each comment with a reply link for that comment.

<% comments.each do |comment| %>  
  <div class="comments-show">
    <div class="comment">
      <p><%= comment.body %><br></p>
      <div class="comment-nav"><a href="#" class="comment-reply">reply</a></div>
      <div class="reply-form">
        <%= form_for @new_comment do |f| %>
          <%= f.hidden_field :commentable_id, value: @new_comment.commentable_id %>
          <%= f.hidden_field :commentable_type, value: @new_comment.commentable_type %>
          <%= f.hidden_field :comment_id, value: comment.id %>
          <div class="field form-group">
            <%= f.text_area :body, class: 'form-control' %>
          </div>
          <div class="field form-group">
            <%= submit_tag "Post Reply", class: 'btn btn-primary' %>
          </div>
        <% end %>
      </div>
    </div>
    <%= render partial: "comments/reply", locals: {comments: comment.children} %>
  </div>
<% end %>  

We will want to add a little style and javascript to the view. First here is the basic style applied to this form template.

.comments-header {
  border-bottom: 1px dotted gray;
  margin-bottom: 10px;
}

.comments-container {
  p {
    margin-bottom: 0px;
  }
  .comment-nav {
    margin-left: 5px;
    margin-bottom: 10px;

    a {
      font-size: 10px;
      font-weight: bold;
      color: #888;
    }
  }
}

.reply-form {
  display: none;
}

.comments-show {
  margin-left: 25px;
}

And for the javascript:

$ ->
  $('.comment-reply').click ->
    $(this).closest('.comment').find('.reply-form').toggle()
    return