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.

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.

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.

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!