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