Building an Angular + Node Comment App using Yeoman

For this tutorial we will be building a simple youtube-style comment box, with the help of the Angular Full-Stack generator.

What it should do:

  • It will require you to be logged in to post comments
  • It will show the name of user who posted the comment
  • It will update comments in real time as they come in

Heres the demo.

Prerequisites

  • MongoDB – Download and Install MongoDB – Make sure mongod is running.

Getting Started

First we’ll download and install generator-angular-fullstack.

npm install -g generator-angular-fullstack

We’ll run the generator and use all the defaults, except we’ll be using Less instead of Sass.

mkdir angular-tube
cd angular-tube
yo angular-fullstack

If everything installed correctly, running grunt serve should start the generated app with a welcome screen. We can leave this terminal open to keep our server alive while we edit it.

Create a new terminal and cd into the project folder for running additional generators.

The project structure

Lets take a moment to look at the project so far so we have a good idea where everything is.

├── client
│   ├── app                 - All of our app specific components go in here
│   ├── assets              - Custom assets: fonts, images, etc… 
│   ├── components          - Our reusable components, non-specific to to our app
│ 
├── e2e                     - Our protractor end to end tests
│
└── server
    ├── api                 - Our apps server api
    ├── auth                - For handling authentication with different auth strategies
    ├── components          - Our reusable or app-wide components
    ├── config              - Where we do the bulk of our apps configuration
    │   └── local.env.js    - Keep our environment variables out of source control.
    │   └── environment     - Configuration specific to the environment the server is being run in
    └── views               - Server rendered views

Thats an overview of the structure, lets drill down little on a client component.

main
├── main.controller.js      - Controller for our main route
├── main.controller.spec.js - Our test
├── main.html               - Our view
├── main.js                 - Registers the route
└── main.less               - Our styles

This structure allows for quite a bit of modularity, but it groups things together logically, so you have an easier time working on a component, and an easier time extracting it out into another project later.

Setting up our main route

Ok so now that we have our project setup, its time to start tearing down the stuff we don’t need.

Lets disable seeding of the database first:

// server/config/environment/development.js
...

seedDB: false,

...

Delete the client/app/main folder, we’ll generate our own, and when prompted set the url to /:

yo angular-fullstack:route main
[?] Where would you like to create this route? client/app/
[?] What will the url of your route be? (/main) /

And we’ll add the markup for our comment form to the main view:

 


<div ng-include="'components/navbar/navbar.html'"></div>

<div class="container">
  <iframe width="100%" height="410" src="//www.youtube.com/embed/DcJFdCmN98s" frameborder="0"></iframe>

  <form class="comment-form" ng-submit="addComment()" name="commentForm">
    <textarea class="form-control" ng-model="newComment" rows="3" placeholder="Enter a new comment" required></textarea>
    <input class="btn btn-primary" type="submit" ng-disabled="commentForm.$invalid" value="Post">
  </form>

  <ul class="comment-list">
    <li ng-repeat="comment in comments">
      <header>{{ comment.author.name }}</header>
      <p>{{ comment.content }}</p>
    </li>
  </ul>
</div>

If you’re familiar with Angular, this should be pretty self-explanatory. Its just a form for submitting comments, and an ng-repeat for displaying them.

We’ll also add a few styles to our routes less file to make it a little more pleasant to look at:

/* client/app/main/main.less */

.comment-form {
  .clearfix();

  .btn-primary {
    margin-top: 7px;
    float: right;
  }
}

.comment-list {
  padding:0;
  list-style:none;

  header {
    font-size: 16px;
    font-weight: bold;
  }
  li {
    .clearfix();
    margin: 7px 0;
    padding: 5px;
    border: 1px solid #ddd;
  }
}

The view for our comment box is done. Now lets start working on the server side.

Creating our API endpoint

We’ll generate our comments endpoint using the endpoint generator:

yo angular-fullstack:endpoint comment
[?] What will the url of your endpoint to be? /api/comments

The endpoint that generates should be accessible now. If you navigate your browser to localhost:9000/api/comments, you should see an empty array as the response.

Setting up our comment model

Open the newly generated model and create the following Schema.

// server/api/comment/comment.model.js
'use strict';

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var CommentSchema = new Schema({
  content: String,
  date: { type: Date, default: Date.now },
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  }
});

CommentSchema.statics = {
  loadRecent: function(cb) {
    this.find({})
      .populate({path:'author', select: 'name'})
      .sort('-date')
      .limit(20)
      .exec(cb);
  }
};

module.exports = mongoose.model('Comment', CommentSchema);

We’re using mongoose to setup a schema for our documents before they’re saved to the database.
For our comments, we need a content field for the message, a date field for when the comment was created, and an author field – which will hold the id of the user that created the comment.

We also can setup static methods that will help us define some custom actions on our model. In this case, we setup a static method for loading the 20 most recent comments. It will populate the comments with the name of the user, based on the comments author id, this is similar to a join in sql.

Comment routes

Because posting a comment requires a user, lets setup routing to only allow logged in users to add comments.

// server/api/comment/index.js
'use strict';

var express = require('express');
var controller = require('./comment.controller');
var auth = require('../../auth/auth.service');

var router = express.Router();
 
router.get('/', controller.index);
router.post('/', auth.isAuthenticated(), controller.create);
router.delete('/:id', auth.isAuthenticated(), controller.destroy);

module.exports = router;

The isAuthenticated method allows us to restrict routes to only users who are logged in. It also attaches the authenticated user to req.user, which we will use in our controller.

Comment controller

Our comment controller is where we handle the request and response for our endpoint. We want to use the loadRecent method, that we just added to the model, to find the recent comments populated with the authors information, and return that as the response. We also want to save the authors user id whenever a comment is created.

// server/api/comment/comment.controller.js

...

// Get list of comments
exports.index = function(req, res) {
  Comment.loadRecent(function (err, comments) {
    if(err) { return handleError(res, err); }
    return res.json(200, comments);
  });
};

// Creates a new comment in the DB.
exports.create = function(req, res) {
  // don't include the date, if a user specified it
  delete req.body.date;

  var comment = new Comment(_.merge({ author: req.user._id }, req.body));
  comment.save(function(err, comment) {
    if(err) { return handleError(res, err); }
    return res.json(201, comment);
  });
};

...

We’ll also need to edit the comment.socket.js, which is responsible for pushing model events to our client. We just need to populate comments with the authors information before we emit it.

// server/api/comment.socket.js

...

function onSave(socket, doc, cb) {
  Comment.populate(doc, {path:'author', select: 'name'}, function(err, comment) {
    socket.emit('comment:save', comment);
  });
}

...

Hooking up our client to our server

Our api and server are done. We just need to have the two communicate. We’ll do that through our Angular controller.

// client/app/main.controller.js

'use strict';

angular.module('angularTubeApp')
  .controller('MainCtrl', function ($scope, $http, socket) {
    $scope.newComment = '';

    // Grab the initial set of available comments
    $http.get('/api/comments').success(function(comments) {
      $scope.comments = comments;

      // Update array with any new or deleted items pushed from the socket
      socket.syncUpdates('comment', $scope.comments, function(event, comment, comments) {
        // This callback is fired after the comments array is updated by the socket listeners

        // sort the array every time its modified
        comments.sort(function(a, b) {
          a = new Date(a.date);
          b = new Date(b.date);
          return a>b ? -1 : a<b ? 1 : 0;
        });
      });
    });

    // Clean up listeners when the controller is destroyed
    $scope.$on('$destroy', function () {
      socket.unsyncUpdates('comment');
    });

    // Use our rest api to post a new comment
    $scope.addComment = function() {
      $http.post('/api/comments', { content: $scope.newComment });
      $scope.newComment = '';
    };
  });

So with the syncUpdates method, we’re listening for saves or deletes on the comment model, and pushing any new comments into our array. This allows us to keep the comments updated across clients in real time.

We’re done!

And there you have it. Now if you still have your server running, you should be able to add comments, and have them sync in real time, and only authenticated users are allowed to post! Pretty cool, eh?

So we’ve learned how we can build a simple full stack application. Check out the documentation for the generator and consider reading about how to deploy to heroku or openshift with the generator, its really easy! Thanks for following along and have fun!

Share Button
  • Kjellski

    Wohooooo, you’ve done a great job, again Tyler, big thanks for you OSS contributions here! And shame on me for not helping you out more… Keep up the good work!

  • DoctorCobweb

    awesome tutorial & thank you for angular-fullstack. it rocks!

  • Shirsih

    Is there any forum / group for angular fullstack where users can ask best practice questions on framework ?

  • Diego Cardozo

    Good work, I have one question, “_.merge” is a plugin for mongoose or it’s already on the core?

    • tylerhenkel

      Thats part of lodash.

  • Jogster

    Hi Tyler, Great example. Love what you’ve done with the generator. I found that there were a few errors in the example. I learnt a heck of a lot from making it work, but I will share the errors I found.

    The first is in the comment.controller.js — Comment.save(function(..){}); -> Comment.create(req.body, function(…){});

    function save does not exist.

    mai.controller.js

    need to add:

    $scope.newComment = ”;

    ->>>> $scope.comments = [];

    as the socket.syncUpdates gets confused when it does not get an array.

  • Celina

    Hi Tyler, I was wondering if you had a an opinion on how best to extend this example when a comment should only be sent from one user to another targeted users.

    The way I was experimenting was connecting a:
    {‘logged in userIDs’: ‘unique socket’} store within /config/socketio.js
    & but i m having trouble making this accessible within server/api/comment.socket.js
    - so that a unique “socket” may be replaced ‘socket’.emit(‘comment:save’, comment).
    I’d greatly appreciate any pointers. My other thought was to replace passport with passport.socket ?

    Many thanks

  • steve price

    I’m looking for a way to have the user update/ edit a previous comment. How would you extend this to include Update functionality.

  • sph130

    Do you have the source for this.. i’m obviously missing something getting a blank page on load. (Or something has been updated since..)

    • michaeljota

      Hi! I have the same problem. Did you manage to solve it?

      • http://www.chicagoworks.com cklanac

        Change your path to http://localhost:9000/main

        When you ran ‘yo angular-fullstack:route main’ it created a new route ‘/main’.
        See ‘main/main.js’


        .state('main', {
        url: '/main',
        templateUrl: 'app/main/main.html',
        controller: 'MainCtrl'
        });

    • James Gailey

      Hey guys, the reason you’re getting a blank page is because on the last step you’re probably editing the main.js file (I did the same thing) but you should actually be editing the main.controller.js file.

      I think Tyler had a typo in that last section, the comments show “// client/app/main.js” but it should actually be “// client/app/main.controller.js”

      • tylerhenkel

        Thanks, I updated the post.

  • Tommy

    This is great. Thank you.

  • Diego

    Thanks for the tutorial.
    I have a problem with req.user._id it seems is undefined, I think I can be forgetting something

  • http://felixrios.com/ Felix Rios

    Thanks for the great tutorial! Really helpful

  • http://ardentkid.com Omid Ahourai

    Awesome! Would like to see a follow-up with Articles implementation too!

  • Ismael Covarrubias

    Hello, great tutorial, I’m trying to get the whole picture of the Daftmonk generator and this tutorial is great.

    By the way where/how is the onSave function described above is used? I suppose you have to require the comment.event.js module, but I didn’t know what to do with it, can you give me a hand there?

  • Khoai Nhu

    Would you please post the full server/api/comment.socket.js ? I really appreciate that

  • Khoai Nhu

    I run yo angular-fullstack:endpoint comment, but it did not create server/api/comment.socket.js . Did I miss something ?

    • Garth Dimond

      Yes the last question before fullstack generates asks do you want to use socksts? you need to answer Y

  • Sunny

    Thanks for the tutorial.
    I have been working on this. I keep getting the error ” Typeerror: Cannot read property ‘_id’ of undefined. ” Can someone help me ?

  • Sunny

    Thanks for the tutorial.
    I have been working on this. I keep getting the error ” Typeerror: Cannot read property ‘_id’ of undefined.” Can someone help me with it ?

  • Sunny

    Can I get the complete code of this app anywhere?

  • http://www.codecpm.in CodecPM

    is there any way to customise endpoint urls, right now iam trying to add “/latest” url in endpoint routes but its throwing CastException, because after “/” everything treating as id.
    If I want achieve this kind of requirement what should I do?

  • Wade Bekker

    I’m assuming socket.io should’ve been installed up front? This small details would really help us beginners.

  • http://zenva.com/ Pablo Farías

    I’m trying to use the Postman Chrome extension to interact with the API’s created with this yeoman generator, but get “Error: CSRF token missing” error. How can we modify these generated API’s to be usable with Postman?

    • tauqeer shakir

      You need to replace csrf: { angular: true } with csrf: false in server/config/express.js file

  • Gaël Dpt

    The post populate don’t work for me..
    I resolved this problem with that:

    // server/api/comment.event.js

    function emitEvent(event) {
    return function(doc) {
    Comment.populate(doc, {path:’author’, select: ‘name’}, function(err, comment) {
    CommentEvents.emit(event + ‘:’ + comment._id, comment);
    CommentEvents.emit(event, comment);
    });
    };
    }
    You can remove the onSave() function in comment.socket.js

  • http://www.ratracegrad.com/ Jennifer Bland

    Do you have an updated version of this tutorial to work with the latest version of the generator angular fullstack? New version uses gulp not grunt and it is in es6 format.

  • https://fried.com/ Jafar

    Yeoman generators can save you a huge amount of time writing boilerplate, and give you a great foundation for building apps. Today we’ll be looking at a workflow for making applications with an Angular, Express, and Node stack using the Yeoman angular-fullstack generator.

    Jafar

  • Fried

    The Node interface is the primary datatype for the entire Document Object Model. It represents a single node in the document tree. While all objects implementing the Node interface expose methods for dealing with children, not all objects implementing the Node interface may have children. For example, Text nodes may not have children, and adding children to such nodes results in a DOMException being raised.

    Fried