We all know, Comments are every where Blogs, Social Networking sites, e-learning forums etc.

In this article, we've got an Article model and we want to create a nested commenting system onto that. Such that Article can have comments, comments can have replys, replys can have more replys and so on.

For example -

This is an article
     This is an comment
         this is a reply
             this is a reply's reply
                 .... (so on)
      This is another comment.

Now, we're going to talk about polymorphic associations and what they are and how they can be implemented for nested commenting system.

What is Polymorphic Association ?

Using polymorphic associations, a model can belong to more than one other model, on a single association.
For example, you might have a picture model that belongs to either an employee model or a product model. - Retrieved from Rails documentation

Example for how it should be declared:

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

Now lets talk about nested commenting.

Models

For this tutorial, we’ll be having a new Rails app. We’re only going to add what we need to show in this feature, so for starters, let’s create the article model, so it’s just a Title and the body.

rails new nested_comments_app
rails generate model Article title:string body:text
rake db:migrate

We decided to use “text” for the body, so we’re not limited by length. Now let’s add the model for comments.

rails generate model Comment body:text commentable:references{polymorphic}
rake db:migrate

Now that we have these two models created, we need to create the associations between them. Lets start with Articles. An article can have many comments, so we need to add that. However, Rails would normally assume that comments would have a column called “article_id” which it doesn’t so we need include the name we gave to the polymorphic association:

  class Article < ActiveRecord::Base
     has_many :comments, as: :commentable
  end

Since a comment can also have many comments, we’re going to include the same thing in the Comments model. But before that, we need to let Rails know that Comments can belong to more than one model (articles or comments), so we need to specify that it belongs to a polymorphic association.

  class Comment < ActiveRecord::Base
      belongs_to :commentable, polymorphic: true
      has_many :comments, as: :commentable
  end

Now let’s see how this works. Lets check in Rails console:

   rails c

Now lets create some entries in article model

  Article.create(title: 'This is the title for article', body: 'this is the body for article')

We then want to add a comment to that article, to make sure our associations work properly. Let’s enter it through the comments association with the article, to mimic if we were to leave a comment on a article on the article’s show page. We can just use Article.first since it’s the only entry in our database.

    Article.first.comments.create(body: "This is a comment on article!")

When we write Comment.new in rails console, we’ll see that Rails knew that the commentable type was Article, since it was a comment on a article, and that it used the article id (1, since this was our first entry) as the commentable_id.

Now let’s add a comment to that first comment, creating our first nested comment. We could do it the same way:

   Comment.first.comments.create(body: "This is a reply on comment!")

Now when we check this comment we'll see commentable type as Comment and commentable id will have comment's id

Now our models are setup correctly.

Routes

Now lets see how routes should be created

config/routes.rb

  Rails.application.routes.draw do
      root 'articles#index'

      resources :articles do
          resources :comments
      end

      resources :comments do
          resources :comments
      end
   end

Let’s see if everything is routing correctly, this will help you see how it’s all nested. In the Terminal, check your routes with:

  rake routes

Try to look and understand the routes.
This is what you should see:

Prefix                  Verb           URI Pattern                           Controller#Action
root                    GET                 /                                 articles#index
article_comments        GET    /articles/:article_id/comments(.:format)comments#index 
                        POST   /articles/:article_id/comments(.:format)       comments#create
new_article_comment     GET    /articles/:article_id/comments/new(.:format)      comments#new
edit_article_comment    GET    /articles/:article_id/comments/:id/edit(.:format) comments#edit
 article_comment        GET    /articles/:article_id/comments/:id(.:format)      comments#show
                        PATCH  /articles/:article_id/comments/:id(.:format)      comments#update
                        PUT    /articles/:article_id/comments/:id(.:format)      comments#update
                        DELETE /articles/:article_id/comments/:id(.:format)      comments#destroy
        articles        GET    /articles(.:format)                               articles#index
                        POST   /articles(.:format)                               articles#create
     new_article        GET    /articles/new(.:format)                           articles#new
    edit_article        GET    /articles/:id/edit(.:format)                      articles#edit
         article        GET    /articles/:id(.:format)                           articles#show
                        PATCH  /articles/:id(.:format)                           articles#update
                        PUT    /articles/:id(.:format)                           articles#update
                        DELETE /articles/:id(.:format)                           articles#destroy
comment_comments        GET    /comments/:comment_id/comments(.:format)          comments#index
                        POST   /comments/:comment_id/comments(.:format)          comments#create
new_comment_comment     GET    /comments/:comment_id/comments/new(.:format)      comments#new
edit_comment_comment    GET    /comments/:comment_id/comments/:id/edit(.:format) comments#edit
 comment_comment        GET    /comments/:comment_id/comments/:id(.:format)      comments#show
                        PATCH  /comments/:comment_id/comments/:id(.:format)      comments#update
                        PUT    /comments/:comment_id/comments/:id(.:format)      comments#update
                        DELETE /comments/:comment_id/comments/:id(.:format)      comments#destroy
        comments        GET    /comments(.:format)                               comments#index
                        POST   /comments(.:format)                               comments#create
     new_comment        GET    /comments/new(.:format)                           comments#new
    edit_comment        GET    /comments/:id/edit(.:format)                      comments#edit
         comment        GET    /comments/:id(.:format)                           comments#show
                        PATCH  /comments/:id(.:format)                           comments#update
                        PUT    /comments/:id(.:format)                           comments#update
                        DELETE /comments/:id(.:format)                           comments#destroy

Now lets talk about the controllers
Controllers

As we all know controllers are the main brain behind an application which allows the model to talk to the view. So in our case there are two different models. So we would require two controllers Articles and Comments Controllers.

** app/controllers/articles_controller.rb**

  class ArticlesController < ApplicationController

    before_action :find_article, only: [:show, :edit, :update, :destroy]

    def index
      @articles=Article.all
    end

    def show
    end

    def new
      @article=Article.new
    end

    def create
      @article= Article.new(article_params)
      redirect_to @article if @article.save
    end

    def edit
    end

    def update
      redirect_to @article if @article.save(article_params)
    end

    def destroy
      redirect_to :back if @article.destroy
    end

    private

    def find_article
      @article = Article.find(params[:id])
    end

    def article_params
      params.require(:article).permit(:title, :body)
    end

  end

Now we move onto the comments controller. Here we don’t need index and show, as comments always will be on article view pages, and don’t have their own pages. But we’ll need new and create, because we want to create new comments. We also need to create a method to let Rails know if we’re creating a comment for a Article or for a comment.

** app/controllers/comments_controller.rb**

  class CommentsController < ApplicationController

  before_action :find_commentable

    def new
      @comment = Comment.new
    end

    def create
      @comment = @commentable.comments.new(comment_params)

      if @comment.save
        redirect_to :back, notice: 'Your comment was successfully posted!'
      else
        redirect_to :back, notice: "Your comment wasn't posted!"
      end
    end

    private

    def comment_params
      params.require(:comment).permit(:body)
    end

    def find_commentable
      @commentable = Comment.find_by_id(params[:comment_id]) if params[:comment_id]
      @commentable = Article.find_by_id(params[:article_id]) if params[:article_id]
    end
  end

As our comments are nested within other comments or articles, we’re using the instance variable @commentable in the create action. We have a private method (find_commentable) that is telling Rails that if the params contains a comment_id, it’s a comment on a comment, and if it has article_id, it’s a comment on an article. We then added a filter at the top of the controller, telling Rails to run the private method before performing any other action.

Lastly, we need to create the views to see all the code we written works.

Views

So there are four things we need for all of this to work. We need a show page for a article, and index page for articles, a way to see comments, and a way to post comments.

Let’s start with the index page and show page. The index page will display all of the articles in our database, along with a link to the show page for each article.

app/views/articles/index.html.erb

  <h1>Articles</h1>
  <% @articles.each do |article| %>
    <p>
      <%= link_to(article.title, article.body, target: "_blank") %> - <%= link_to("Show Page", article) %>
    </p>
  <% end %>

Now we need the show page for each article, this page will have details about the article, a form for submitting a comment about the article, the display of each comment, and the ability to comment on a comment.

app/views/articles/show.html.erb

  <%= link_to(@article.title, @article.body, target: "_blank") %><br/>
  <small>Submitted <%= time_ago_in_words(@article.created_at) %> ago</small>

  <h3>Comments</h3>

    <%= form_for [@article, Comment.new] do |f| %>
    <%= f.text_area :body, placeholder: "Add a comment" %><br/>
    <%= f.submit "Add Comment" %>
  <% end %>

  <ul>
    <%= render @article.comments %>
  </ul>

As we have broken the file into partials, we need to add that view file.

apps/views/comments/_comment.html.erb

  <li>
    <%= comment.body %> -
    <small>Submitted <%= time_ago_in_words(comment.created_at) %> ago</small>

    <%= form_for [comment, Comment.new] do |f| %>
        <%= f.text_area :body, placeholder: "Add a Reply" %><br/>
        <%= f.submit "Reply"  %>
        <% end %>

    <ul>
        <%= render comment.comments %>
    </ul>

  </li>

As we’re passing this partial the collection of comments, it displays each comment and the form for replying to that comment. But then it renders itself within itself, to show the replies each comment might have. So by using the ul/li structure, we’re making sure they all nest correctly when they display.

Run the server, and take a look at what we did.

rails server

Because we created a article, a comment, and a reply already, so you should see them all. Then we can see by adding a new comment on that article, and then a reply to that comment.

If you want to see the bitbucket repo for this project, it’s posted here.

References:
https://github.com/rails/rails