Most blogs allow the reader to interact with the content by posting comments. Let’s add some simple comment functionality.
First, we need to brainstorm what a comment is…what kinds of data does it have…
- It’s attached to an article
- It has an author name
- It has a body
With that understanding, let’s create a
Comment model. Switch over to your terminal and enter this line:
rails generate model Comment author_name:string body:text article:references
We’ve already gone through what files this generator creates, we’ll be most interested in the migration file and the
Open the migration file that the generator created,
db/migrate/some-timestamp_create_comments.rb. Let’s see the fields that were
class CreateComments < ActiveRecord::Migration[5.1]
Once that’s complete, go to your terminal and run the migration:
The power of SQL databases is the ability to express relationships between elements of data. We can join together the information about an order with the information about a customer. Or in our case here, join together an article in the
articles table with its comments in the
comments table. We do this by using foreign keys.
Foreign keys are a way of marking one-to-one and one-to-many relationships. An article might have zero, five, or one hundred comments. But a comment only belongs to one article. These objects have a one-to-many relationship – one article connects to many comments.
Part of the big deal with Rails is that it makes working with these relationships very easy. When we created the migration for comments we started with a
references field named
article. The Rails convention for a one-to-many relationship:
- the objects on the “many” end should have a foreign key referencing the “one” object.
- that foreign key should be titled with the name of the “one” object, then an underscore, then “id”.
In this case one article has many comments, so each comment has a field named
Following this convention will get us a lot of functionality “for free.” Open your
app/models/comment.rb and check it out:
class Comment < ActiveRecord::Base
The reason this
belongs_to field already exists is because when we generated the Comment model, we included this line:
article:references. What that does is tell Rails that we want this model to reference the Article model, thus creating a one-way relationship from Comment to Article. You can see this in action in our migration on the line
A comment relates to a single article, it “belongs to” an article. We then want to declare the other side of the relationship inside
app/models/article.rb like this:
class Article < ActiveRecord::Base
belongs_to :article was implemented for us on the creation of the Comment model because of the references, the
has_many :comments relationship must be entered in manually.
Now an article “has many” comments, and a comment “belongs to” an article. We have explained to Rails that these objects have a one-to-many relationship.
Let’s use the console to test how this relationship works in code. If you don’t have a console open, go to your terminal and enter
rails console from your project directory. If you have a console open already, enter the command
reload! to refresh any code changes.
Run the following commands one at a time and observe the output:
article = Article.first
When you called the
comments method on object
article, it gave you back a blank array because that article doesn’t have any comments. When you executed
Comment.new it gave you back a blank Comment object with those fields we defined in the migration.
But, if you look closely, when you did
article.comments.new the comment object you got back wasn’t quite blank – it has the
article_id field already filled in with the ID number of article
article. Additionally, the following (last) call to
article.comments shows that the new comment object has already been added to the in-memory collection for the
article article object.
Try creating a few comments for that article like this:
comment = article.comments.new
For the first comment,
comment, I used a series of commands like we’ve done before. For the second comment,
new_comment, I used the
new doesn’t send the data to the database until you call
create you build and save to the database all in one step.
Now that you’ve created a few comments, try executing
article.comments again. Did your comments all show up? When I did it, only one comment came back. The console tries to minimize the number of times it talks to the database, so sometimes if you ask it to do something it’s already done, it’ll get the information from the cache instead of really asking the database – giving you the same answer it gave the first time. That can be annoying. To force it to clear the cache and lookup the accurate information, try this:
You’ll see that the article has associated comments. Now we need to integrate them into the article display.
We want to display any comments underneath their parent article. Open
app/views/articles/show.html.erb and add the following lines right before the link to the articles list:
This renders a partial named
"comment" and that we want to do it once for each element in the collection
@article.comments. We saw in the console that when we call the
.comments method on an article we’ll get back an array of its associated comment objects. This render line will pass each element of that array one at a time into the partial named
"comment". Now we need to create the file
app/views/articles/_comment.html.erb and add this code:
Display one of your articles where you created the comments, and they should all show up.
Good start, but our users can’t get into the console to create their comments. We’ll need to create a web interface.
The lazy option would be to add a “New Comment” link to the article
show page. A user would read the article, click the link, go to the new comment form, enter their comment, click save, and return to the article.
But, in reality, we expect to enter the comment directly on the article page. Let’s look at how to embed the new comment form onto the article
Just above the “Back to Articles List” in the articles
<%= render partial: 'comments/form' %>
This is expecting a file
app/views/comments/_form.html.erb, so create the
app/views/comments/ directory with the
_form.html.erb file, and add this starter content:
<h3>Post a Comment</h3>
Look at an article in your browser to make sure that partial is showing up. Then we can start figuring out the details of the form.
First look in your
articles_controller.rb for the
Remember how we created a blank
Article object so Rails could figure out which fields an article has? We need to do the same thing before we create a form for the
But when we view the article and display the comment form we’re not running the article’s
new method, we’re running the
show method. So we’ll need to create a blank
Comment object inside that
show method like this:
@comment = Comment.new
Due to the Rails’ mass-assignment protection, the
article_id attribute of the new
Comment object needs to be manually assigned with the
id of the
Article. Why do you think we use
Comment.new instead of
Now we can create a form inside our
comments/_form.html.erb partial like this:
<h3>Post a Comment</h3>
Save and refresh your web browser and you’ll get an error like this:
Showing C:/laragon/www/blogger/app/views/comments/_form.html.erb where line #2 raised:
form_for helper is trying to build the form so that it submits to
article_comments_path. That’s a helper which we expect to be created by the router, but we haven’t told the router anything about
Comments yet. Open
config/routes.rb and update your article to specify comments as a sub-resource.
resources :articles do
Then refresh your browser and your form should show up. Try filling out the comments form and click SUBMIT – you’ll get an error about
uninitialized constant CommentsController.
Did you figure out why we aren’t using
@article.comments.new? If you want, edit the
show action and replace
@comment = Comment.new with
@comment = @article.comments.new. Refresh the browser. What do you see?
For me, there is an extra empty comment at the end of the list of comments. That is due to the fact that
@article.comments.new has added the new
Comment to the in-memory collection for the
Article. Don’t forget to change this back.
Just like we needed an
articles_controller.rb to manipulate our
Article objects, we’ll need a
Switch over to your terminal to generate it:
rails generate controller comments
The comment form is attempting to create a new
Comment object which triggers the
create action. How do we write a
You can cheat by looking at the
create method in your
articles_controller.rb. For your
comments_controller.rb, the instructions should be the same just replace
There is one tricky bit, though! We need to assign the article id to our comment like this:
As a user, imagine you write a witty comment, click save, then what would you expect? Probably to see the article page, maybe automatically scrolling down to your comment.
At the end of our
create action in
CommentsController, how do we handle the redirect? Instead of showing them the single comment, let’s go back to the article page:
article_path needs to know which article we want to see. We might not have an
@article object in this controller action, but we can find the
Article associated with this
Comment by calling
Test out your form to create another comment now – and it should work!
We’ve got some decent comment functionality, but there are a few things we should add and tweak.
Let’s make it so where the view template has the “Comments” header it displays how many comments there are, like “Comments (3)”. Open up your article’s
show.html.erb and change the comments header so it looks like this:
<h3>Comments (<%= @article.comments.size %>)</h3>
The comments form looks a little silly with “Author Name”. It should probably say “Your Name”, right? To change the text that the label helper prints out, you pass in the desired text as a second parameter, like this:
<%= f.label :author_name, "Your Name" %>
comments/_form.html.erb so it has labels “Your Name” and “Your Comment”.
We should add something about when the comment was posted. Rails has a really neat helper named
distance_of_time_in_words which takes two dates and creates a text description of their difference like “32 minutes later”, “3 months later”, and so on.
You can use it in your
_comment.html.erb partial like this:
<% if comment.created_at != nil %>
With that, you’re done!
Now that the comments feature has been added push it up to GitHub:
git add .