Learn Rails - Polymorphism

Question Click to View Answer

In this test we will created nested comments using polymorphism. There will be an articles model that has many comments and comments can be responded to by other users and should be displayed in a nested format (similar to Reddit or Hacker News). Start by creating a new rails project called nested_comments_polymorphism.

$ rails new nested_comments_polymorphism

Create the Article MVC with scaffolding. The Article resource should have body and title attributes.

$ rails generate scaffold Article title:string body:text

Create the Comment MVC template with the generate resource command. The comment resource should have a body attribute.

$ rails generate resource Comment body:text 

Create the articles and comments tables in the database.

$ rake db:migrate

Update the routes so that comments can be made on articles and other comments (we will add a reply link to comments, so comments can be made on other comments).

config/routes.rb
root :to => 'articles#index'     

resources :articles do     
  resources :comments    
end     

resources :comments do    
  resources :comments     
end   

View the routes that this routes.rb file generates.

$ rake routes

Update the comments table so it will support a polymorphic relationship.

$ rails generate migration AddColumnsToComments
# Update the migration with the following changes:
class AddColumnsToComments < ActiveRecord::Migration
  def change
    add_column :comments, :commentable_id, :integer
    add_column :comments, :commentable_type, :string  
  end
end

Update the Article model, so it supports a polymorphic relationship.

class Article < ActiveRecord::Base
  attr_accessible :body, :title
  has_many :comments, :as => :commentable, :dependent => :destroy    
end

Update the Comment model, so it supports a polymorphic relationship.

class Comment < ActiveRecord::Base
  attr_accessible :body
  belongs_to :commentable, :polymorphic => true     
  has_many :comments, :as => :commentable
end

Put a form on the article#show page to make new comments.

# views/articles/show.html.erb
<%= render 'comments/form' %>

# views/comments/_form.html.erb
<%= form_for [@article, @comment = Comment.new] do |f| %>
  <%= f.text_area :body %> <br>
  <%= f.submit %>    
<% end %>

comments_controller.rb
def create
  @parent = Article.find(params[:article_id]) if params[:article_id]   
  @parent = Comment.find(params[:comment_id]) if params[:comment_id]  
  @comment = @parent.comments.new(params[:comment])
  if @comment.save
    redirect_to article_path(params[:article_id])
  else
    render 'new'
  end
end

The create action determines if the parent for a given comment is an article or another comment. The @comment = @parent.comments.new(params[:comment]) associates the comment with its parent.

Display all comments on the articles#show page.

# views/articles/show.html.erb
<%= render @article.comments %>

# views/comments/_comment.html.erb
<%= comment.body %><br />

The render @article.comments in the show page automatically knows to load a file called views/comments/_comment.html.erb. This is Rails magic, so just memorize this.

Make some comments in the form on the articles#show page and see how they are stored in the database.

$ rails db
>> select * from comments;

Notice that all of the comments have a commentable_type of Article. All the comments currently have an Article parent because we have not added the functionality to reply to a comment and have a Comment as the parent - we will add this function later.

After each comment on the articles#show page, add a reply link to create a new child comment.

# views/articles/show.html.erb
<%= render 'comments/form' %>

# views/comments/_form.html.erb
<%= form_for [@article, @comment = Comment.new] do |f| %>
  <%= f.text_area :body %> <br>
  <%= f.submit %>    
<% end %>

# comments_controller.rb
def new
  @parent_comment = Comment.find(params[:comment_id])     
  @comment = @parent_comment.comments.new 
end

Show all the nested comments with the child comments more indented than the parent comments.

# comments/_comment.html.erb 
<div class="comment"> <%= render :partial => comment.comments %></div>    

# stylesheets/comments.css.scss
.comment {
  margin-left: 30px;
}

Add a link to delete comments and all comments nested under that comment.

# views/comments/_comment.html.erb
<%= link_to "Destroy", article_comment_path(comment.commentable, comment), :method => :delete %>

# comments_controller.rb
def destroy
  @comment = Comment.find(params[:id])    
  @comment.destroy
  redirect_to articles_path
end