10. Comments on Comments
So last lesson we were thinking through how to put comments inside other comments. Now we are going to actually do it!
- Create a post
- Show all posts
- Show one post
- Comment on posts
- Create subreddits
- Sign up and Login
- Associate posts and comments with their author
- Make comments on comments
- Optimize our how we populate.
- Make comments embedded inside comments.
- Test that comments on comments are working.
- Vote a post up or down
Optimize our how we populate
We know we're going to be embedding comments into comments. This means we're going to have to nest populate calls for comments for as deep as a comment chain can go! Our current implementation can't scale for that, so before we embark on embedding comments, we need to make sure we can handle them by optimizing how we use populate.
Populate util#
We're going to be using our optimization in a lot of places, so we should make it a util so that it can be easily accessible through multiple places
[info] Any time you have the same functionality used in multiple areas of a project, it's best to make a
utilfolder where you can store all your functions for repeated use.
[action] Create a
utilfolder, and create anautopopulate.jsfile in itIn
autopopulate.js, place the following code:
module.exports = (field) => function (next) { this.populate(field) next() }Great! Because this function recursively calls itself, we can now populate fields every time we load a model! Let's make some changes so that our posts and comments take advantage of this functionality.
[action] Update
/models/post.jsand/models/comment.jsto use your newutil. We also need to update the schema in/models/comment.jsto include a newcommentsproperty so that comments can support comments of their own:
/models/post.js
// models/post.jsconst { Schema, model } = require('mongoose');const Populate = require('../util/autopopulate');>const postSchema = new Schema({...});// Always populate the author fieldpostSchema .pre('findOne', Populate('author')) .pre('find', Populate('author'));>module.exports = model('Post', postSchema);
/models/comment.js
const { Schema, model } = require('mongoose');const Populate = require('../util/autopopulate');>const commentSchema = new Schema({ content: { type: String, required: true }, author : { type: Schema.Types.ObjectId, ref: 'User', required: true }, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }],}, { timestamps: true });>// Always populate the author fieldcommentSchema .pre('findOne', Populate('author')) .pre('find', Populate('author')) .pre('findOne', Populate('comments')) .pre('find', Populate('comments'));>module.exports = model('Comment', commentSchema);Finally, let's update our controllers to simplify their logic to just use the lean method.
[action] Simplify your
/posts/show/and/subreddit/show/methods in/controllers/post.js
// SHOWapp.get('/posts/:id', (req, res) => { const currentUser = req.user; Post.findById(req.params.id).populate('comments').lean() .then((post) => res.render('posts-show', { post, currentUser })) .catch((err) => { console.log(err.message); });});>// SUBREDDITapp.get('/n/:subreddit', (req, res) => { const { user } = req; Post.find({ subreddit: req.params.subreddit }).lean() .then((posts) => res.render('posts-index', { posts, user })) .catch((err) => { console.log(err); });});Notice it was only a one line change for each, but it's a lot simpler now! One more refactor to go:
[action] Update
/comments/createin/controllers/commentsto the following:
const Post = require('../models/post');const Comment = require('../models/comment');>module.exports = (app) => { // CREATE Comment app.post('/posts/:postId/comments', (req, res) => { const comment = new Comment(req.body); comment.author = req.user._id; comment .save() .then(() => Promise.all([ Post.findById(req.params.postId), ])) .then(([post]) => { post.comments.unshift(comment); return Promise.all([ post.save(), ]); }) .then(() => res.redirect(`/posts/${req.params.postId}`)) .catch((err) => { console.log(err); }); });};[challenge]
Refactor all the code blocks above to be async/await.
Now would be a good point to refresh your screen and make sure the authors on your comments and posts are still showing correctly. Notice also we are now redirecting users back to the post's page instead of root. Makes a lot more sense to go back to where you originated the request to reply from, doesn't it?
We did a lot of changes here, so let's commit what we have so far:
Now Commit#
$ git add .$ git commit -m 'refactored populate code'$ git pushYou have now successfully investigated how to use the populate method in Mongoose for advanced associations. Congrats!
Credit#
All credit to Dacio Romero (a Make School student!) for creating this elegant solution! He even wrote a plugin to do exactly what we wrote above for our util!
Embedding Comments Into Comments
Now that we have autopopulate, we can make it so a user can comment on a comment to create a nice hierarchical tree of nested comments.
Again, utilize the user experience as a starting point. A user who wants to make a comment on a comment can click a "reply" link that opens up a form displaying the specific comment and a textarea, as well as a Reply button.
[action] Add this snippet to
/views/posts-showwithin theeachblock forcomments:
{{#each post.comments}}<p>{{this.content}}</p><p class="text-right">{{this.author.username}}</p><a href="/posts/{{../post._id}}/comments/{{this._id}}/replies/new">Reply</a>{{/each}} ...Let's make a new replies.js file in our controllers folder. Within, we'll need access to both the Post and Comment models. At the end, we'll have two freshly implemented routes: NEW and CREATE.
[action] create
/controllers/repliesand include the following code:
const Post = require('../models/post');const User = require('../models/user');const Comment = require('../models/comment');>module.exports = (app) => { // NEW REPLY app.get('/posts/:postId/comments/:commentId/replies/new', (req, res) => { const currentUser = req.user; let post; Post.findById(req.params.postId).lean() .then((p) => { post = p; return Comment.findById(req.params.commentId).lean(); }) .then((comment) => { res.render('replies-new', { post, comment, currentUser }); }) .catch((err) => { console.log(err.message); }); });> // CREATE REPLY app.post('/posts/:postId/comments/:commentId/replies', (req, res) => { console.log(req.body); });};Remember to
requireyour new controller inserver.js
require("./controllers/posts")(app)require("./controllers/comments.js")(app)require("./controllers/auth.js")(app)require("./controllers/replies.js")(app)Next, create your replies-new template and have the content sit in the middle 6 columns of the 12 column grid:
[action] Create
/views/replies-newin your/viewsfolder and include the following code:
<div class="row"> <div class="col-sm-6 col-sm-offset-3"> <form action="/posts/{{post._id}}/comments/{{comment._id}}/replies" method="post"> <div class="form-group"> <textarea name="content" class="form-control" id="reply-content" placeholder="Reply"></textarea> </div> > <div class="text-right"> <button type="submit" class="btn btn-primary">Reply</button> </div> </form> </div></div>Refresh the page and click the Reply button on a comment. We can see that our template and form is there, but what happens when we submit it? We made the create route and left a console.log(req.body), so if you submit that form, you should see the console log the content of the form submission. Do you see it?
The next step is to write our /replies/create route logic.
[action] Update the
/createmethod in/controllers/repliesto the following:
// CREATE REPLYapp.post("/posts/:postId/comments/:commentId/replies", (req, res) => { // TURN REPLY INTO A COMMENT OBJECT const reply = new Comment(req.body) reply.author = req.user._id // LOOKUP THE PARENT POST Post.findById(req.params.postId).then((post) => { // FIND THE CHILD COMMENT Promise.all([reply.save(), Comment.findById(req.params.commentId)]) .then(([reply, comment]) => { // ADD THE REPLY comment.comments.unshift(reply._id) return Promise.all([comment.save()]) }) .then(() => res.redirect(`/posts/${req.params.postId}`)) .catch(console.error) // SAVE THE CHANGE TO THE PARENT DOCUMENT return post.save() })})When you submit the reply form, what occurs? Can you confirm (in the database) that a nested embedded comment document is created?
Finally, let's set up our post-show template to show these sub comments as well once they are created. If we just try to manually write in our comments and their comments, we won't be able to represent the whole tree. We'll need to use a Partial Template to make a recursive representation of all the comments and their comments.
[action] In
/views/posts-show, replace your{{#each post.comments}}code block with the following:
{{#each post.comments}} {{> comment comment=this postId=../post._id}} {{/each}}We've got our view set up to use our partial comments view, so now let's create that partial.
[action] Create a new folder in
viewscalledpartials, then create a file in there calledcomment.handlebars. Inside that file, place the following code:
<div class="col-xs-12 comment-indent"> <p>{{comment.content}}</p> <p class="text-right">{{comment.author.username}}</p> <a href="/posts/{{postId}}/comments/{{comment._id}}/replies/new">Reply</a> {{#each comment.comments}} {{> comment comment=this postId=../postId}} {{/each}}</div>The code inside this partial template calls itself so it loops until every comment is displayed.
Finally, let's give each comment a bit of an indent by creating a style for the class .comment-indent. Let's put this in a general stylesheet in case we want to add more styles later.
[action] Create a
publicfolder and create acssfolder within it. Within thecssfolder, create anall.cssfile and place the following code in it:
.comment-indent { margin-left: 20px;}Remember we need to now link the style sheet in
/views/layouts/main.handlebarsso that we can view it:
<head> ... <link rel="stylesheet" href="/css/all.css" /></head>[challenge]
Refactor all the code blocks above to be async/await.
Product So Far
Refresh and try out your new nested comments!!

Now that we have a css folder, what other styles would you add? Add some more styling to spruce up your Reddit!
Credit#
Credit again to Dacio Romero, who's /controllers/replies/post method was adapted to this tutorial, as well as his partials template!
Now Commit
$ git add .$ git commit -m 'Implemented nested comments'$ git pushStretch Challenge
[challenge] Notice right now you can reply to a comment if you are not logged in. Update your code to figure out how to only allow logged in users to reply to comments