× {{alert.msg}} Never ask again
Receive New Tutorials
GET IT FREE

Building a Reddit-like Commenting System with Rails

– {{showDate(postTime)}}

This article will walk you through how to create threaded comments in rails using polymorphic associations. The tutorial was written by Mark Webster from the Shipyard Team. Mark and his co-founders are learning how to code through Codementor’s Featured Star program, and this tutorial is based on what he has learned so far.

“It takes a village.” It’s an appropriate way to describe learning to code. Without the wealth of online tutorials, blog posts, and StackOverflow answers, this journey would be exponentially more difficult. And the majority of that content has been shared for no other reason than a passion and enthusiasm for helping others.

While still very much a newbie myself, I feel I now have an opportunity to give back a bit and write my first tutorial. I recently implemented threaded commenting using polymorphic associations in my coding project, and share the details below in the spirit of reciprocal generosity.

As someone new to Rails, this comes with the disclaimer that there are many other ways to build this. The approach outlined here would likely need to be revisited for performance reasons at scale. You can also find gems that offer similar solutions. But it’s always good to build things from scratch when learning, so you have a good understanding of how things work. And the tutorial below is fairly straightforward, and most importantly, it works. Hopefully you will find it helpful.

Approach

Comments are an important part of building engaging communities online. They give users a chance to share their thoughts about a blog post, video, or piece of content. When comments are nested, or threaded, they also give users a chance to engage directly with one another, creating lively discussions and debates. Massive platforms like YouTube and reddit are driven by their threaded commenting features.

Let’s use reddit as the starting point for this tutorial. We’ll assume your project involves someone submitting a story link to share on the site. Other users can then comment directly on that story, or comment on another user’s comment.

To build this, you could also use one model, with one column for “story_id” and one for “comment_id” (meaning one column would always be nil). Or you could use polymorphic associations.

With a polymorphic association, we’ll create one Comments model. There will then be one column for “commentable_id”, which will store the ID of the object we’re commenting on, and then “commentable_type”, which will indicate what type of object we’re commenting on (in this case, a Story or a Comment).

Models

For this tutorial, we’ll be starting with a brand new Rails app (using Rails 4). We’re only going to add what we need to demonstrate this feature, so for starters, let’s create the stories model. This is what users submit to our Reddit-like site, so it’s just a Title and URL (that the title would link out to).

rails g model story title:text url:string
rake db:migrate

So now we have the objects that make the core of our user experience. We decided to use “text” for the title, so we’re not limited by length (string is limited to 255 characters). Now let’s add the model for comments.

rails g model comment body:text commentable_id:integer commentable_type:string
rake db:migrate

Now that we have these two models created, we need to create the associations between them. The easiest place to start is with Stories. A story can have many comments, so we need to add that. However, Rails would normally assume that comments would include a column called “story_id” which it doesn’t so we need include the name we gave to the polymorphic association:

app/models/story.rb

class Story < 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 (stories or comments), so we need to specify that it belongs to a polymorphic association.

apps/models.comment.rb

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

So to recap, a Story can have many comments. A Comment can have many comments. And since a comment can belong to more than one model, we specify that a comment belongs to a polymorphic association through commentable.

Now let’s see how this works. Fire up your trusty Rails console:

rails c

Since we have no entries in our database for our Reddit-like site, let’s create one:

Story.create(title: "Live 1:1 help from expert developers", url: "http://www.codementor.io")

You should then see this entry be created:

INSERT INTO "stories" ("title", "url", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Live 1:1 help from expert developers"], ["url", "http://www.codementor.io"], ["created_at", "2016-01-11 22:02:54.637379"], ["updated_at", "2016-01-11 22:02:54.637379"]]

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

Story.first.comments.create(body: "What a helpful resource!")

And we should see the following:

INSERT INTO "comments" ("body", "commentable_id", "commentable_type", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["body", "What a helpful resource"], ["commentable_id", 1], ["commentable_type", "Story"], ["created_at", "2016-01-11 22:08:03.701599"], ["updated_at", "2016-01-11 22:08:03.701599"]]

You’ll see that Rails knew that the commentable_type was Story, since it was a comment on a story, and that it used the story_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 threaded/nested comment. We could do it the same way:

Comment.first.comments.create(body: "I agree, very helpful!")

And we should see the following:

INSERT INTO "comments" ("commentable_id", "commentable_type", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["commentable_id", 1], ["commentable_type", "Comment"], ["body", "I agree, very helpful"], ["created_at", "2016-01-11 22:33:35.018312"], ["updated_at", "2016-01-11 22:33:35.018312"]]

Success! Our models are setup correctly, and all the data flows the way it should.

Routes

As the traffic cop for Rails, we have to tell our routes.rb what to do with web requests. First we’ll tell the app to use the index page for stories as the root for this site. Then we’ll add the resources for stories and comments. This will create all of the default actions (index, show, new, edit, create, update and destroy) for each controller. But to each, we’ll also add a nested resource, to nest those actions for comments within each set of routes. So it looks like this:

config/routes.rb

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

  resources :stories do
    resources :comments
  end

  resources :comments do
    resources :comments
  end

end

Let’s see if everything is routing correctly. Since we didn’t limit the default routes, we’ll see more than we need (for instance, we won’t have a comments Index page), but this will help you see how it’s all nested. In the Terminal, check your routes with:

rake routes

This is what you should see:

Prefix Verb   URI Pattern                                       Controller#Action
                root GET    /                                                 stories#index
      story_comments GET    /stories/:story_id/comments(.:format)             comments#index
                     POST   /stories/:story_id/comments(.:format)             comments#create
   new_story_comment GET    /stories/:story_id/comments/new(.:format)         comments#new
  edit_story_comment GET    /stories/:story_id/comments/:id/edit(.:format)    comments#edit
       story_comment GET    /stories/:story_id/comments/:id(.:format)         comments#show
                     PATCH  /stories/:story_id/comments/:id(.:format)         comments#update
                     PUT    /stories/:story_id/comments/:id(.:format)         comments#update
                     DELETE /stories/:story_id/comments/:id(.:format)         comments#destroy
             stories GET    /stories(.:format)                                stories#index
                     POST   /stories(.:format)                                stories#create
           new_story GET    /stories/new(.:format)                            stories#new
          edit_story GET    /stories/:id/edit(.:format)                       stories#edit
               story GET    /stories/:id(.:format)                            stories#show
                     PATCH  /stories/:id(.:format)                            stories#update
                     PUT    /stories/:id(.:format)                            stories#update
                     DELETE /stories/:id(.:format)                            stories#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

Success again! You’ll see there are stories, there are comments, then there are comments nested within stories, and comments nested within comments. Now that the requests are going where they need to go, let’s make sure we’re providing the proper response for each.

Controllers

As we’re dealing with stories and comments, we’ll need to create a controller for each. We’ll start with stories. For the sake of brevity in this tutorial, we’re going to skip creating, editing, or deleting stories, since we have at least one to already work with. So we’ll just handle the index and show requests, so that we can view the story we created. Notice that we don’t have to address comments at all here.

app/controllers/stories_controller.rb

class StoriesController < ApplicationController
  def index
    @stories = Story.all
  end

  def show
    @story = Story.find(params[:id])
  end
end

Now we move onto the comments controller. Here we don’t need index and show, as comments always live on story 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 story 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 = Story.find_by_id(params[:story_id]) if params[:story_id]
    end

end

Since our comments are nested within other comments or stories, 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 story_id, it’s a comment on a story. We then added a filter at the top of the controller, telling Rails to run the private method before performing any other action (otherwise it wouldn’t know what @commentable was when it got to the create action).

Lastly, we need to create the views to see all this magic we created work.

Views

So there are four elements we need for all of this to work. We need a show page for a story, and index page for stories, 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 stories in our database, along with a link to the show page for each story.

app/views/stories/index.html.erb

<h1>Stories</h1>
<% @stories.each do |story| %>
  <p>
    <%= link_to(story.title, story.url, target: "_blank") %> - <%= link_to("Show Page", story) %>
  </p>
<% end %>

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

app/views/stories/show.html.erb

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

<h3>Comments</h3>

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

<ul>
  <%= render(partial: 'comments/comment', collection: @story.comments) %>
</ul>

You’ll see that we broke out some elements into a partial. So 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 partial: 'comments/comment', collection: comment.comments %>
  </ul>

</li>

Because 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 (recursive!), to display the replies each comment might have. And by using the ul/li structure, we’re making sure they all nest correctly when they display. Fancy.

See Our Handiwork in Action

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

rails s 

Because we created a story, a comment, and a reply already, so you should see them all. Then play around with adding a new comment on that story, and then a reply to that comment.

If you add the new and create actions and new view to Stories, you will basically have the core functionality of reddit built. Not bad for a quick tutorial.

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

Good luck!

Missed our earlier posts? Catch-up on our journey to finally learning to code with our previous posts:


About the Author

This article was written by Mark Webster from ShipyardNYC. Mark and his two other co-founders, Vipin and Minesh, are participating in Codementor’s Featured Stars Program, where they will work with a dedicated mentor to bolster their understanding of programming concepts and become better entrepreneurs.




Questions about this tutorial?  Get Live 1:1 help from Ruby on Rails experts!
Taiwo
Taiwo
5.0
Experienced fullstack developer and co-founder
Thanks for checking out my profile. I have over a decade of full-stack dev experience with languages/frameworks like Ruby, Rails, Javascript,...
Hire this Expert
Oladayo
Oladayo
5.0
Back-end Developer at interviewHall
I am a seasoned programmer with over 5 years of software development. I will help solve any programming problems as well as refine your ideas....
Hire this Expert
comments powered by Disqus