Create You Own CMS With Angularjs From Scratch

Download as pdf or txt
Download as pdf or txt
You are on page 1of 16

As we proceed, take a moment to verify your current version of Node.

js and upgrade it if
necessary. The status of the latest version and how to upgrade it can be found on the Node.js
site at http://nodejs.org/.

Create You own CMS with Angularjs From scratch


In this Tutorial, we will learn to build your own Content Management System (CMS). This
Tutorial iS for Beginners as well as for Expert Developers

Setting up MongoDB

Until now, we have been heavily dependent on external web services to handle all our
backend server-side work. Now, we will build our own backend using MongoDB, ExpressJS,
AngularJS, and Node.js; all these together are also popularly known as the MEAN stack.

Depending on your operating system, MongoDB can be installed in multiple ways.


Perform the steps mentioned at the following links to install MongoDB on your operating
system:

This Tutorial will focus more on making AngularJS work smoothly with a backend system.
As we get through this tutorial , some of the interesting things that well learn are as follows:

Building RESTful web services using Node.js and ExpressJS


Saving and reading data from MongoDB
Working with ExpressJS and AngularJS routes within the same application

For Windows, refer to http://docs.mongodb.org/manual/tutorial/install-mongodb-onwindows/


For Mac OS X, refer to http://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/
For Ubuntu, refer to http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/

Once you have installed MongoDB, the next most important step is to create the folder to
store your data.

Why the MEAN stack?


An obvious question would be why the choice of MongoDB, Node.js, and Express, when we
could use any other stack.
To be politically correct, we could use any other technology, such as Java, PHP, ASP.NET,
or even Ruby on Rails, to build the backend part of this project, and AngularJS would work
just as fine.

Create an empty folder named data/db on the root using the following command line:
mkdir /data/db

You can also create the folder directly in the C: as follows:


c:\md data
c:\md data\db

The main reason to choose this stack is that all the tools within this stack use a single
language, which is JavaScript. Other than this, each of the following tools offers certain
unique benefits that make it equally suitable to build this application:

Next, well connect to the MongoDB database using the following command:
mongod

Node.js: This is the most important tool in this stack. It allows us to build eventdriven, nonblocking I/O applications using JavaScript. Thanks to Node.js, we are now
able to write server-side applications in JavaScript.
ExpressJS: This is a lightweight web application framework that allows us to build a
server-side application on Node.js using the Model View Controller (MVC) design
pattern.
MongoDB: This is a very popular NoSQL database. It uses JavaScript to read and
modify data, and the data is stored in the Binary JSON (BSON) format.
MongooseJS: This is an object modeling tool for MongoDB. It provides a schemabased approach to model our data and also a much easier way to validate and query
data in MongoDB.

Tip

You will need to either give read or write permissions to the data/db folder or use the sudo
or admin privilege to run the MongoDB command with root-level privileges.
In the following steps, we will start the mongo shell and create a new database named
angcms.
With MongoDB running in a terminal window, we will open a new terminal window and fire
the following commands.
mongo
use angcms

MongoDB comes with a default test database; one can also use this to test and play around
with some MongoDB command

Getting started with the MEAN stack


Lets start by installing the various tools that well need to build our application.

Setting up ExpressJS and MongooseJS

By this time, you probably already have Node.js installed and have become reasonably
comfortable with starting and stopping the web servers.

In case you dont have ExpressJS yet, you can install it using the following command:
1

npm install -g express-generator

The next step is to create your ExpressJS project folder, which will be done using the
following command:
express angcms

This will create a folder named angcms and put the boilerplate Express files into it. Note that
we still dont have ExpressJS installed; we will need to install it with the following command
from the terminal:

var mongoose = require('mongoose');


var Schema = mongoose.Schema;
var Page = new Schema({
title: String,
url: {type:String, index:{unique:true}},
content: String,
menuIndex: Number,
date: Date
});
var Page = mongoose.model('Page', Page);
module.exports=Page;

The following table gives a description for the fields in the schema:

npm install

Description

Fields

Well now install MongooseJS as a devdependency along with ExpressJS.


title

The title of the content page.

url

The SEO-friendly alias that will be used to identify the page. Note that we are setting its
index to unique as we dont want duplicate URL aliases.

content

The content of the page.

Save the file, cd, into the angcms folder, and run the following command:
npm install -save mongoose

Go to the angcms/node_modules folder, and verify that we have the express, jade, and
mongoose folders within it.

menuIndex An integer that defines the menu sequence of the pages in the navigation bar.

Lets also check whether our server is working by firing the following command in the
terminal:

date

The date when this document was last updated.

npm start

Open the browser and run http://localhost:3000; you should get the Welcome to Express
message.

Next, we create the schema for our admin users in the models/admin-users.js file as
follows:

Building the server-side app

var mongoose = require('mongoose');


var Schema = mongoose.Schema;
var adminUser = new Schema({
username: String,
password: String
});
var adminUser = mongoose.model('adminUser', adminUser);
module.exports=adminUser;

Well start by building the server-side section of the app. Well build a series of routes that
will provide Create, Read, Update, Delete (CRUD) operations on our MongoDB database.
We will expose these as REST APIs.
Lets write our models and custom routes into a separate route file to keep things clean.

Creating the Mongoose schemas

As you can see, we are keeping things very simple, with our admin users schema only
storing the username and password.

We first start by loading the mongoose library and establishing a connection to the angcms
database. We add the following highlighted code in the angcms/app.js file:

Creating CRUD routes


Now, well start writing the routes for the CRUD operations; well start by generating the
listing page.

var app = express();


var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/angcms');
var db = mongoose.connecetion;

Create a new file, routes/api.js, in the routes folder, and add the following code:

For this application, we are going to need two schemas: the Pages schema and the Admin
Users schema. Lets create these now.
Well create a new folder named models, and create our page.js file with the following code
in it:

var
var
var
var
var

express = require('express');
router = express.Router();
mongoose = require('mongoose');
Page= require('../models/page.js');
adminUser= require('../models/admin-users.js');

/* User Routes. */

}
router.get('/', function(req, res) {
res.send('Welcome to the API zone');
});

});
});

As we need to pass data to our server script, we will use the post method instead of get.
Next, we create a new instance of our page object and pass the request parameters from our
post data. We then call the save method, which does the actual task of saving this data into
the collection.

router.get('/pages', function(request, response) {


return Page.find(function(err, pages) {
if (!err) {
return response.send(pages);
} else {
return response.send(500, err);
}
});

We can test this route by simulating the post action using either the browsers developer
tools console or Firebug console. Alternatively, there are quite a few REST clients available
as browser extensions and add-ons that can help you simulate the post action.

});
module.exports = router;

What the preceding code does is that it runs the find() method on the Page schema and
returns the list of pages found. In case of an error, it would return a status code of 500 and
display the error message. We need to get back to our app.js file and add the following lines
to create these routes:

Try to create a couple of pages using this method, and run


http://localhost:3000/api/pages to verify that this data is being saved and returned as a
JSON response. Youll also notice an additional key named _id being saved along with each
of these nodes. We will be using the _id key for our delete and update operations
Updating a collection

Once we have the route to save a new entry, the next logical step is to create our route that
will allow us to update an entry. Well continue to write the code to modify a collection item
in our angcms/routes/api.js file as follows:

var api = require('./routes/api');


app.use('/api', api);

Add the preceding two lines within the respective sections of the app.js file.

router.post('/pages/update', function(request, response) {


var id = request.body._id;

Make sure that app.use('/api', api); is called before app.use('/', routes);. This will
ensure that the /api routes get higher priority than the others.

Page.update({
_id: id
}, {
$set: {
title: request.body.title,
url: request.body.url,
content: request.body.content,
menuIndex: request.body.menuIndex,
date: new Date(Date.now())
}
}).exec();
response.send("Page updated");

On the terminal, stop and restart the npm using the npm start command. Note that you need
to restart the web server every time you make a change to the server-side code.
On the browser, navigate to http://localhost:3000/api/pages.
You should see empty square brackets. This means that our current collection is empty.
Adding a new entry to the collection

});

Deleting a collection item

Next, lets write the route to add data to our collection. We will continue adding it to the
routes/api.js file as follows:

Next comes the route to delete an item; while continuing to work on the same file, we add the
following code:

router.post('/pages/add', function(request, response) {


var page = new Page({
title: request.body.title,
url: request.body.url,
content: request.body.content,
menuIndex: request.body.menuIndex,
date: new Date(Date.now())
});

router.get('/pages/delete/:id', function(request, response) {


var id = request.params.id;
Page.remove({
_id: id
}, function(err) {
return console.log(err);
});
return response.send('Page id- ' + id + ' has been deleted');
});

page.save(function(err) {
if (!err) {
return response.send(200, page);

Ensure that the code is working by testing it with a REST client or typing in the route URL in
a browser window along with a valid ID.

} else {
return response.send(500,err);

Displaying a single record

Using bcrypt to encrypt passwords

Next, we will write the route to fetch the data for an individual page on the admin side.

To encrypt confidential data such as passwords, we will use a popular utility called bcrypt to
hash the password before it is stored in the database.

Well continue by adding the following code to our api.js file:


Lets download and install the bcrypt-nodejs package using the following terminal
command from the root of the project folder:

router.get('/pages/admin-details/:id', function(request, response) {


var id = request.params.id;
Page.findOne({
_id: id
}, function(err, page) {
if (err)
return console.log(err);
return response.send(page);
});
});

npm install bcrypt-nodejs

Next, we will include this in our ExpressJS app. As we will be securing our routes, well
include the bcrypt module in our angcms/routes/api.js file as follows:
var bcrypt = require('bcrypt-nodejs');

Adding a new admin user

We use the get method here and pass the ID as a request parameter. We then run the
findOne method to pull up a single record that matches the ID and return that as a response.

Along with this, we will create our route to add in a new admin user as follows:

You can easily verify this route by simply appending the ID to the URL endpoint as follows:
http://localhst:3000/api/pages/view/<_id>.

On similar lines, we will also create another route to fetch the page contents for the frontend.
Here, in the following code, we will use the URL as a parameter to fetch the data because we
would like our frontend to show SEO-friendly URLs:

router.post('/add-user', function(request, response) {


var salt, hash, password;
password = request.body.password;
salt = bcrypt.genSaltSync(10);
hash = bcrypt.hashSync(password, salt);
var AdminUser = new adminUser({
username: request.body.username,
password: hash
});
AdminUser.save(function(err) {
if (!err) {
return response.send('Admin User successfully created');

router.get('/pages/details/:url', function(request, response) {


var url = request.params.url;
Page.findOne({
url: url
}, function(err, page) {
if (err)
return console.log(err);
return response.send(page);
});
});

});

Securing your admin section

Here, we first start by defining our password, salt, and hash variables.

Now, its time to secure the admin section so that only authorized users can log in.

Then, using bcrypt and salt, we generate the hash string of the password.

An important thing to note here is that we will need to secure both the client-side admin
section and also our server-side APIs, because it is relatively easy to bypass client-side
validations.

Note

} else {
return response.send(err);
}
});

Using the salt variable is optional with bcrypt, but it is recommended, as it makes it
difficult for potential hackers to decrypt the hashed password.

We will start with securing our server-side code. ExpressJS comes with its own session
management and encryption modules.
We will enable cookieParser in our app by adding the following line to our angcms/app.js
file:

We then create a new instance of the AdminUser object, store the username and hashed
password, and run the save method to save this information in the AdminUser document in
MongoDB.

Creating the route for authenticating login


app.use(express.cookieParser('secret'));

Next, we create the route for login. Add the following code to the api.js file:
7

router.post('/login', function(request, response) {


var username = request.body.username;
var password = request.body.password;

As of ExpressJS Version 4.x, all the middleware, except static, have been removed and need
to be installed and included as needed. Thus, we download our session module with the
following terminal command:

adminUser.findOne({
username: username
}, function(err, data) {
if (err | data === null) {
return response.send(401, "User Doesn't exist");
} else {
var usr = data;

npm install express-session --save

We then include the following lines in the respective sections of our app.js file:
var session = require('express-session');
app.use (session());

if (username == usr.username && bcrypt.compareSync(password,


usr.password)) {

Next, we write our function that will check the user sessions. We add this to the api.js file:

request.session.regenerate(function() {
request.session.user = username;
return response.send(username);

function sessionCheck(request,response,next){
if(request.session.user) next();
else response.send(401,'authorization failed');

});
} else {
return response.send(401, "Bad Username or Password");
}

Now, to secure the API routes, we simply need to call the sessionCheck function after the
route name, as highlighted in the following code:

}
});
});

router.post('/pages/add', sessionCheck, function(request, response) {

The code piece, although long, is fairly straightforward.

Usually, wed want to secure the APIs that modify the data, and hence, we will add the
sessionCheck function to the add, update, and delete APIs as follows:

We capture the username and password as variables from the post data. We then check to
see if the username is present, and if it is, then using the compare method of bcrypt, we
check to see if the password entered matches that stored in the database.

For the update API, it should be as follows:


router.post('/pages/update', sessionCheck, function(request,
response) {

Once the username and password match, we create the user session and redirect the user to
the pages listing page.

In case the username or password doesnt exist, we return back with a status code 401 and a
relevant error message.

For the delete API, it should be as follows::


router.get ('/pages/delete/:id', sessionCheck,
function(request,response){

We will be using this status code in our AngularJS side to redirect the users in case of session
time outs and so on.

Creating the logout route

For the details API, it should be as follows::


router.get('/pages/admin-details/:id', sessionCheck,
function(request, response) {

Integrating AngularJS with an ExpressJS project


After the login function, we create the logout function as follows:
Now that we have most of our server-side code working, well start working on our
AngularJS code.

router.get('/logout', function(request, response) {


request.session.destroy(function() {
return response.send(401, 'User logged out');

Lets download the angular-seed project as a ZIP download from


https://github.com/areai51/angular-seed and extract the contents of the ZIP file.

});
});

Now, we will only take the content of the app folder along with the package.json and
bower.json files and place it within the public folder of angcms.

The function will simply destroy the session.

Writing the sessionCheck middleware

In the terminal, navigate to the angcms/public folder and run the following two
commands:

The next step is to create our middleware function that does a session check.
9

10

npm install
bower install

To make our site URLs are SEO friendly, we need to turn on the HTML5 mode in
$locationProvider by making the following highlighted changes in the
angcms/public/js/app.js file:

Note that we do not run npm start from within the public folder, as we will be using
the Express server that runs at port 3000.

.config(['$routeProvider', '$locationProvider', function($routeProvider,


$locationProvider) {
$routeProvider.when('/view1', {templateUrl: 'partials/partial1.html',
controller: 'MyCtrl1'});
$routeProvider.when('/view2', {templateUrl: 'partials/partial2.html',
controller: 'MyCtrl2'});
$routeProvider.otherwise({redirectTo: '/view1'});
$locationProvider.html5Mode(true);
}]);

Your folder structure should look something like the following:

The next thing to do is set the base URL in our angcms/public/index.html file, as
highlighted in the following code:
<title>AngCMS</title>
<base href="/">
<link rel="stylesheet" href="css/bootstrap.min.css"/>

Refresh the Index page, and you will notice that your URLs are now clean without the #
symbol in them.

Building the admin section for CRUD operations


We will now look to build the admin section of our CMS using Angular JS. The AngularJS
app will talk to the backend ExpressJS scripts that we just wrote in the preceding section.

Creating the routes for the admin section


The next step is to define the routes in our ExpressJS app such that all routes are managed
by AngularJS, except for those that start with a/api/.
For this, we will add the following catch-all route at the end of the
angcms/routes/index.js file as follows:

Ideally, we would like our admin section to be called from within the admin URL, so lets go
ahead and add the routes for the admin section of the AngularJS app.
Add the following routes to the angcms/public/js/app.js file:
config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {

router.get('*', function(request, response) {


response.sendfile('./public/index.html');
});

$routeProvider.when('/admin/login', {
templateUrl: 'partials/admin/login.html',
controller: 'AdminLoginCtrl'
});
$routeProvider.when('/admin/pages', {
templateUrl: 'partials/admin/pages.html',
controller: 'AdminPagesCtrl'
});
$routeProvider.when('/admin/add-edit-page/:id', {
templateUrl: 'partials/admin/add-edit-page.html',
controller: 'AddEditPageCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);

The routes in ExpressJS are executed sequentially, and hence, the catch-all route needs to
be at the end.
Restart your app.js node application and point the browser URL to
http://localhost:3000/index.html. Verify that the page displayed is the default
index.html file of angular-seed.

Generating SEO-friendly URLs using HTML5 mode


All this while, all the URLs in our AngularJS app have had # in the URLs. When building a
CMS, ensuring that the URLs are meaningful and SEO-friendly is quite important.
}
]);

11

12

For the admin side, we have three routes: /admin/login is to authenticate the user,
/admin/pages will show the list of pages available, and /admin/add-edit-page/:id will
be used to add or edit the contents of the page. Note that we will make use of a single route to
both add and edit a page.

Building the controllers for the admin section


Now that we have our factory services ready, well get started with writing our controllers.
Well add the following code to the angcms/public/js/controllers.js file:

Building the factory services

'use strict';
angular.module('myApp.controllers', []).
controller('AdminPagesCtrl', ['$scope', '$log', 'pagesFactory',
function($scope, $log, pagesFactory) {
pagesFactory.getPages().then(
function(response) {
$scope.allPages = response.data;
},
function(err) {
$log.error(err);
});

As we are going to be reading the dynamic data from web services, we will create a factory
service that will be used to communicate with the backend web service.
Lets create our factory web services that will do the CRUD operations.
We will add the following methods to our angcms/public/js/services.js file:
'use strict';
angular.module('myApp.services', [])

$scope.deletePage = function(id) {
pagesFactory.deletePage(id);
};

.factory('pagesFactory', ['$http',
function($http) {
return {
getPages: function() {
return $http.get('/api/pages');
},

}
]);

Tip

savePage: function(pageData) {
var id = pageData._id;

Dont forget to delete the default controllers that come as a part of the angular-seed
package.

if (id === 0) {
return $http.post('/api/pages/add', pageData);
} else {
return $http.post('/api/pages/update', pageData);
}
},
deletePage: function(id) {
return $http.get('/api/pages/delete/' + id);
},
getAdminPageContent: function(id) {
return $http.get('/api/pages/admin-details/' + id);
},
getPageContent: function(url) {
return $http.get('/api/pages/details/' + url);
},
};

The AdminPagesCtrl controller is primarily used to display the pages listing.


We make a request to the getPages method of pagesFactory and populate the allPages
scope object using the promise.
We also define our method to delete a page; the method accepts the id value as an input
parameter.

Setting up the admin page layout


Well now work on building our listing view that will display a list of all the pages, along
with the ability to add, edit, or delete a page.

}
]);

The methods to list, delete, and view the details of a page are quite straightforward; we
simply make a request to the appropriate ExpressJS route that passes the id parameter where
necessary.
Focusing on the savePage method, youll notice that we are using the same method to add a
new page or edit the contents of an existing page. What we do here is we check for the id
value in our post data. If the id value is set to 0, then it is treated as adding a new record;
otherwise, it will try to update the record whose id value is being passed.

13

Before we get to our listing view, lets first get the groundwork ready on our Index page
located at angcms/public/index.html.
Ensure that your index.html file contains the following code:
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="utf-8">
<title>Angular CMS</title>
<base href="/">
<link rel="stylesheet"
href="bower_components/bootstrap/dist/css/bootstrap.min.css" />

14

<link rel="stylesheet"
href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" />
<link rel="stylesheet" href="css/app.css" />
</head>

</tr>
</table>

<body>
<div class="container" ng-view></div>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
<script src="js/directives.js"></script>
</body>
</html>

We will leverage BootStrap3 to get our styling in place. You can choose to either download
Bootstrap from www.getbootstrap.com, call it from any of the CDN, or run the following
command in the terminal from within the angcms/public folder:

At the top, we have a button to add new pages. It will link to the add-edit-page route and
pass a fixed ID of 0. As you might have realized, we are reusing our partial to add and edit
the page. We will need to let AngularJS know when to call the add endpoint and when to call
the edit endpoint. For this reason, we pass 0 as a parameter while adding a new page and the
MongoDB-assigned ID while editing a page.
The next piece of code is the table to display our list of pages with the title and URL fields.
Along with it, we also have links to edit or delete the respective page. Both these hyperlinks
link to the respective routes that pass the page ID.
Save the file and point the browser URL to http://localhost:3000/admin/pages . This
should show you a list of pages. In case you dont see any pages, check for any console errors
or add some content using a REST Client for the time being, until our add-edit-page route
is ready.
The delete link will not work for now as its API is authenticated.

bower install bootstrap

Setting up authentication in AngularJS


As you can see from the code, we are loading bootstrap and Bootstrap-theme CSS files to
take advantage of the default Bootstrap theme.

Before we can proceed to build the client-side sections, well need to build the login and
session management modules in AngularJS. Well need to do this now, because the rest of
the services for the CRUD operation are secured on the server side.

The only other change to the index.html file at this stage is adding the container CSS
class to our ng-view div. This will act as the container for all the pages that load within it.

Creating our login page

Building the listing view for the admin section


Next, well create the partial that will display our list of pages stored in the database.

We will start with the creation of our partial by creating a new file in
angcms/public/partials/admin/login.html, and we will put in the following code:

Create a folder named admin and a new file named pages.html at


angcms/public/partials/admin/pages.html, and add the following

<h1>Login</h1>
<hr/>

code:

<form role="form" id="login" ng-submit="login(credentials)">

<a href="#/admin/add-edit-page/0" class="btn btn-success pull-right"> Add


New Page</a>
<h1>Pages List</h1>
<hr/>
<table class="table">
<thead>
<tr>
<th>Menu Index</th>
<th>Title</th>
<th>URL</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>

<div class="form-group">
<label>Login</label>
<input class="form-control" type="text" ng-model="credentials.username"/>
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="
credentials.password"/>
</div>
<input type="submit" class="btn btn-success" value="Login">
</div>
</form>

<tr ng-repeat="page in allPages">


<td>{{page.menuIndex}}</td>
<td>{{page.title}}</td>
<td>{{page.url}}</td>
<td> <a ng-href="#/admin/add-edit-page/{{page._id}}">Edit</a>
</td>
<td> <a ng-href="#" ng-click="deletePage(page._id)">Delete</a>
</td>

Next, we will create our controller in the angcms/public/js/controllers.js file with the
following code.
.controller('AdminLoginCtrl', ['$scope', '$location', '$cookies',
'AuthService','$log',

15

16

function($scope, $location, $cookies, AuthService, $log) {


$scope.credentials = {
username: '',
password: ''
};
$scope.login = function(credentials) {
AuthService.login(credentials).then(
function(res, err) {
$cookies.loggedInUser = res.data;
$location.path('/admin/pages');
},
function(err) {
$log.log(err);
});
};
}

}])

Lets test our login functionality. Open the following URL in the browser, and log in with the
correct username and password:
http://localhost:3000/admin/login

Using the correct username and password, you should get redirected to the pages listing.
Note

Make sure you have a couple of admin users saved; if not, use a REST API Client and create
a couple of admin users using the following API URL:

])

http://localhost:3000/api/add-user

Youll notice that we have injected $location, AuthService, $scope, $log, and $cookies
into our controller function.

Setting up authentication in AngularJS

AngularJS has a module called ngCookies that allows to read and write to the browser
cookie. However, this doesnt come as a part of the AngularJS library and needs to be
included separately.
Run the following command in the terminal to download angular-cookies:

Before we can proceed to build the client-side sections, well need to build the login and
session management modules in AngularJS. Well need to do this now, because the rest of
the services for the CRUD operation are secured on the server side.

bower install angular-cookies

Creating our login page

Well first need to load the angular-cookies.js file in our angcms/public/index.html


file as follows:

We will start with the creation of our partial by creating a new file in
angcms/public/partials/admin/login.html, and we will put in the following code:

<script type="text/javascript" src="bower_components/angularcookies/angular-cookies.js"></script>

<h1>Login</h1>
<hr/>

Next, we need to include the ngCookies module as a part of our main application. We do this
in our angcms/public/js/app.js file, as highlighted in the following code:

<form role="form" id="login" ng-submit="login(credentials)">


<div class="form-group">
<label>Login</label>

angular.module('myApp', [
'ngRoute',
'myApp.filters',
'myApp.services',
'myApp.directives',
'myApp.controllers',
'ngCookies'
])

<input class="form-control" type="text" ng-model="credentials.username"/>


</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="
credentials.password"/>
</div>

Next, we will create the AuthService factory that will contain the login and logout methods.
Add the following code in the angcms/public/js/services.js file:

<input type="submit" class="btn btn-success" value="Login">


</div>
</form>

.factory('AuthService', ['$http', function($http) {


return {
login: function(credentials) {
return $http.post('/api/login', credentials);
},
logout: function() {
return $http.get('/api/logout');
}
};

Next, we will create our controller in the angcms/public/js/controllers.js file with the
following code.
.controller('AdminLoginCtrl', ['$scope', '$location', '$cookies',
'AuthService','$log',
function($scope, $location, $cookies, AuthService, $log) {
$scope.credentials = {
username: '',

17

18

password: ''
};
$scope.login = function(credentials) {
AuthService.login(credentials).then(
function(res, err) {
$cookies.loggedInUser = res.data;
$location.path('/admin/pages');
},
function(err) {
$log.log(err);
});
};

Lets test our login functionality. Open the following URL in the browser, and log in with the
correct username and password:
http://localhost:3000/admin/login

Using the correct username and password, you should get redirected to the pages listing.
Note

Make sure you have a couple of admin users saved; if not, use a REST API Client and create
a couple of admin users using the following API URL:

}
])

Youll notice that we have injected $location, AuthService, $scope, $log, and $cookies
into our controller function.

http://localhost:3000/api/add-user

Building a custom module for global notification

AngularJS has a module called ngCookies that allows to read and write to the browser
cookie. However, this doesnt come as a part of the AngularJS library and needs to be
included separately.

As you might have realized by now, our login page works fine as long as we put the correct
credentials; however, when you try with an invalid username or password, the page doesnt
do anything.

Run the following command in the terminal to download angular-cookies:

Tip

bower install angular-cookies

The developer console should, however, show a 401 Unauthorized failed message.

Well first need to load the angular-cookies.js file in our angcms/public/index.html


file as follows:
<script type="text/javascript" src="bower_components/angularcookies/angular-cookies.js"></script>

We will need to build a notification system that displays a message when invalid credentials
are passed. Thinking a few steps ahead, youll realize that we are going to need such
messages displayed on many occasions, for example, when a new page has been created or
updated, or when a page has been deleted.

Next, we need to include the ngCookies module as a part of our main application. We do this
in our angcms/public/js/app.js file, as highlighted in the following code:

In view of this, it is most ideal to build a global notification system that can be used all
throughout our application.

angular.module('myApp', [
'ngRoute',
'myApp.filters',
'myApp.services',
'myApp.directives',
'myApp.controllers',
'ngCookies'
])

AngularJS allows us to create custom modules. These are self-contained modules that can be
easily reused across multiple applications. A custom module is simply a wrapper that holds
different parts of an AngularJS app; these parts can be directives, services, filters, controllers,
and so on.
As you would recall, ngCookies is a similar custom module we just made use of earlier.

Next, we will create the AuthService factory that will contain the login and logout methods.
Add the following code in the angcms/public/js/services.js file:
.factory('AuthService', ['$http', function($http) {
return {
login: function(credentials) {
return $http.post('/api/login', credentials);
},
logout: function() {
return $http.get('/api/logout');
}
};
}])

Building and initializing the message.flash module


We will create a new file named message-flash.js at angcms/public/js/messageflash.js.
We will initialize it with the following code:
angular.module('message.flash', [])

We also need to include this in our app, so lets include the message-flash.js file in our
angcms/public/index.html file, as follows:
<script src="js/message-flash.js"></script>

19

20

Next, we add the message-flash.js file as a dependency in our main module in the
angcms/public/js/app.js file, as highlighted in the following code:

We add the broadcast event to the setMessage method in the message-flash.js file as
highlighted:

angular.module('myApp', [
'ngRoute',
'myApp.filters',
'myApp.services',
'myApp.directives',
'myApp.controllers',
'ui.tinymce',
'ngCookies',
'message.flash'
])

setMessage: function(newMessage) {
message=newMessage;
$rootScope.$broadcast('NEW_MESSAGE')
}

Now, every time the setMessage function is called, we will broadcast the event called
NEW_MESSAGE'.
We will continue to chain our directive to the same module in the message-flash.js file as
follows:

Building the message.flash factory service

.directive('messageFlash', [function() {
return {
controller: function($scope, flashMessageService, $timeout) {
$scope.$on('NEW_MESSAGE', function() {
$scope.message = flashMessageService.getMessage();
$scope.isVisible = true;
return $timeout(function() {
$scope.isVisible = false;
return $scope.message = '';
}, 2500);
})
},
template: '<p ng-if="isVisible" class="alert alertinfo">{{message}}</p>'
}
}
]);

We will chain our factory to the message.flash module in our


angcms/public/js/message-flash.js file, as highlighted in the following code:
angular.module('message.flash', [])
.factory('flashMessageService', ['$rootScope',function($rootScope) {
var message = '';
return {
getMessage: function() {
return message;
},
setMessage: function(newMessage) {
message = newMessage;
}
};
}])

The factory service is quite straightforward. We initialize a variable called message and have
two methods, namely, setMessage and getMessage, which assign and read values to the
message variable.

The directive code is quite interesting. We first listen for the broadcast event, and on its
trigger, we populate $scope.message by calling the getMessage function of
flashMessageService.

Setting up $broadcasts

It is usually a good usability practice to hide the flash message after a few seconds of being
visible; hence, we will add a timeout function that will automatically hide the message in
2500 milliseconds.

Anybody who has tried to pass variables from one controller to another or to a directive
would have realized that it isnt quite straightforward, and one needs to use either rootScope
or set up $watch or $digest to ensure that the scope objects update when the source has
changed.

The last piece of code of the directive is the template code that uses the ng-if directive to
toggle the display. We also use Bootstraps alert CSS classes for some visual elegance.
Now, lets add this directive to our main index.html file, as highlighted in the following
code:

We will face a similar problem here where the message in our directive wouldnt update
when we pass the message from a controller.

<div message-flash> </div>


<div class="container" ng-view></div>

To overcome this, we will set up $broadcast.

Setting a flash message

The broadcast, $broadcast, dispatches an event name to all child scopes. Child scopes use
this as a trigger to execute different functions.

Lets revisit our AdminLoginCtrl function and set a flash message in case the login fails.

In our case, as we dont really have a parent-child relation between the directive and our
controllers, we will set up a broadcast on rootScope itself

We add it to our controller.js file, as highlighted.

21

22

.controller('AdminLoginCtrl', ['$scope', '$location', '$cookies',


'AuthService', 'flashMessageService',function($scope, $location, $cookies,
AuthService, flashMessageService) {
$scope.credentials = {
username: '',
password: ''
};
$scope.login = function(credentials) {
AuthService.login(credentials).then(
function(res, err) {
$cookies.loggedInUser = res.data;
$location.path('/admin/pages');

},
function() {
$log.error('error saving data');
}
);
};
}
])

We start by defining our AddEditPageCtrl controller and injecting the necessary


dependencies. Besides $scope and $log, we need to inject $routeparams to get the route
parameters, the $location module to redirect, flashMessageService to set notifications,
and pagesFactory service.

},
function(err) {
flashMessageService.setMessage(err.data);

Next, we check to see if the page ID being passed is 0; this corresponds to an insert or the
long MongoDB-generated ID, which means well be doing an update.

console.log(err);

In case if its the MongoDB-generated ID, we then need to fetch the data of the page and
populate the edit template. For this, we make a call to the getPageContent factory function,
and using promises, we populate our pageContent scope with the returned data.

});
};
}
])

Lets test our login page with an invalid username and password, and we should be able to
see our flash message.

The next part is writing the savePage function, which will save the contents of the form by
posting it to the savePage factory function. When the promise returns with a success, we
redirect the user back to the listing page.

Creating our Add-Edit page controller

Creating our Add-Edit view

Now that we have our global messaging system in place, lets continue with building the rest
of the admin sections

Now that we have the controller in place, lets work on the form to add and edit the page
content.

Well start to create our controller for adding and editing pages.

Create a new file at angcms/public/partials/add-edit-page.html, and add the


following content:

Create a new controller function in the angcms/public/controllers.js file as follows:


.controller('AddEditPageCtrl', ['$scope', '$log', 'pagesFactory',
'$routeParams', '$location', 'flashMessageService', function($scope, $log,
pagesFactory, $routeParams, $location, flashMessageService) {
$scope.pageContent = {};
$scope.pageContent._id = $routeParams.id;
$scope.heading = "Add a New Page";
if ($scope.pageContent._id !== 0) {
$scope.heading = "Update Page";
pagesFactory.getAdminPageContent($scope.pageContent._id).then(
function(response) {
$scope.pageContent = response.data;
$log.info($scope.pageContent);
},
function(err) {
$log.error(err);
});
}

<h1>{{heading}}</h1>
<hr/>
<form role="form" id="add-page" ng-submit="savePage()">
<div class="form-group">
<label>Page ID</label>
<input class="form-control" type="text" readonly ngmodel="pageContent._id"/>
</div>
<div class="form-group">
<label>Page Title</label>
<input class="form-control" type="text" ng-model="pageContent.title"/>
</div>
<div class="form-group">
<label>Page URL Alias</label>
<input class="form-control"type="text" ng-model="pageContent.url"/>
</div>
<div class="form-group">
<label>Menu Index</label>
<input class="form-control"type="number" ng-model="pageContent.menuIndex"/>
</div>

$scope.savePage = function() {
pagesFactory.savePage($scope.pageContent).then(
function() {
flashMessageService.setMessage("Page Saved Successfully");
$location.path('/admin/pages');

<div class="form-group">

23

24

<label>Page Content</label>
<textarea rows="15" class="form-control" type="text" ngmodel="pageContent.content"></textarea>
</div>
<input type="submit" class="btn btn-success" value="Save">
</div>
</form>

Within the update URL function, we store the value into the pageContent.url property by
using the formatURL filter and passing $scope.pageContent.title as an argument to it.
Next, we need to make the highlighted changes to our partial located at
angcms/public/partials/admin/add-edit-page.html, as highlighted:

Test the add page to ensure that its working.

Writing a custom filter to autogenerate the URL field


Most CMS tools would autogenerate the URL alias based on the title of the page. While
doing this, we will need to ensure that the alias being generated is stripped out of any special
characters and all spaces are ideally replaced by a dash.

<label>Page Title</label>
<input class="form-control" type="text" ng-change="updateURL()" ngmodel="pageContent.title"/>
</div>
<div class="form-group">
<label>Page URL Alias</label>
<input class="form-control"type="text" readonly ngmodel="pageContent.url"/>
</div>

Save the files and test the add-edit page in the browser. Notice the URL field getting updated
automatically as you enter the title field.

We will do this by creating our own custom filter.


Open up the angcms/public/js/filters.js file, and add the following code.

Adding the WYSIWYG editor

'use strict';

Most CMS tools would have a What You See Is What You Get (WYSIWYG) editor. This
allows the content administrators to easily format the text on a page, for example, add
headings, make the text bold or italics, add numbering bullets, and so on.

/* Filters */
angular.module('myApp.filters', [])
.filter('formatURL', [
function() {
return function(input) {
var url = input.replace(/[`~!@#$%^&*()_|+\=?;:'",.<>\{\}\[\]\\\/]/gi, '');
var url = url.replace(/[\s+]/g, '-');
return url.toLowerCase();

Well see how to add TinyMCE, a very popular WYSIWYG editor, to our page content text
area.
Angular UI has a ready-to-use module, which makes it very easy to add TinyMCE to any
form in an AngularJS app.

};

The Angular-UI TinyMCE wrapper can be downloaded from GitHub at


https://github.com/angular-ui/ui-tinymce.

}
]);

Here, we are basically creating a filter called formatURL and taking in the input parameters.
We first remove any special characters that may be present using regex. We then replace all
spaces with a hyphen and return the formatted string in lowercase.
Now, lets see how to use it in our code. We will use this filter in our controller, so lets make
the highlighted changes in our controller file located at
angcms/public/js/controlllers.js:

Alternatively, we can also use bower to download the files.


Assuming that you have already installed bower, run the following command in the terminal;
bower install angular-ui-tinymce --save

This will create a folder called bower_components and download the files within it.
Next, lets include these libraries in our index.html file, as highlighted in the following
code:

.controller('AddEditPageCtrl', ['$scope', '$log', 'pagesFactory',


'$routeParams', '$location', 'flashMessageService','$filter',
function($scope, $log, pagesFactory, $routeParams, $location,
flashMessageService,$filter) {

<script type="text/javascript"
src="bower_components/tinymce/tinymce.min.js"></script>
<script type="text/javascript" src="lib/angular/angular.js"></script>
<script type="text/javascript" src="bower_components/angular-uitinymce/src/tinymce.js"></script>
<script src="lib/angular/angular-route.js"></script>

As you can see, we are injecting the $filter module into our controller.
Next, we create a $scope function as follows:
$scope.updateURL=function(){
$scope.pageContent.url=$filter('formatURL')($scope.pageContent.title);
}

Next, we will add the TinyMCE module as a dependency to our app in the
angcms/public/js/app.js file, as highlighted in the following code:
25

26

angular.module('myApp', [
'ngRoute',
'myApp.filters',
'myApp.services',
'myApp.directives',
'myApp.controllers',
'ui.tinymce',
'ngCookies',
'message.flash'
]).

To test whether our Interceptors are working or not, open up a new tab in the browser in
Incognito or private browsing mode and try to directly put in the URL to edit a page; it would
be something like http://localhost:3000/admin/add-edit-page/<_id>.
It should automatically redirect you to the login page.

Building the frontend of our CMS


All this while, we have been working on the backend and admin sections of the CMS.

This is all that is required to include TinyMCE in our AngularJS app.


Now, we will work on the frontend, the public-facing side of the website.
Now, to add the editor to our angcms/public/partials/admin/add-edit-page.html file,
we will simply call our directive, as highlighted in the following code:
<textarea ui-tinymce rows="15" class="form-control" type="text" ngmodel="pageContent.content"></textarea>

As the public-facing side of the website needs to have a neat layout with a logo, navigation
bar, content area, footer, and so on, we are going to tweak the index page layout.
Update the angcms/public/index.html file with the upcoming changes.

Save the file, and now, try to add or edit a page to notice TinyMCE replace the text area.

As we would like to control some application-level settings such as the logo, footer, and so
on, we first bind AppCtrl to the <body> tag, as shown in the following code:

Setting up an Interceptor to detect responses

<body ng-controller="AppCtrl">

A use case that we need to consider is what happens if the backend web services session
timed out and somebody from the frontend is trying to add, edit, or delete a page.

Next, we add the following markup:

At the instance when the backend service times out, it would return a 401 status code; we
would need to have every AngularJS controller check for this status code and redirect the
user to the login page in case it gets one.
Instead of writing this check on each and every controller, we will make use of an Interceptor
to check every incoming response, and act accordingly.
Lets chain our Interceptor service in our services.js file as follows:
.factory('myHttpInterceptor', ['$q', '$location', function($q, $location) {
return {
response: function(response) {
return response;
},
responseError: function(response) {
if (response.status === 401) {
$location.path('/admin/login');
return $q.reject(response);
}
return $q.reject(response);
}
};
}]);

The next step is to push this into $httpProvider.


We will add the following code to our angcms/public/js/app.js file:
.config(function ($httpProvider) {
$httpProvider.interceptors.push('myHttpInterceptor');
});

27

<div admin-login class="col-md-3 pull-right"></div>


<div class="container">
<header>
<img ng-src="{{site.logo}}">
</header>
<div message-flash></div>
<div class="row">
<div class="col-md-3" nav-bar></div>
<div class="col-md-6" ng-view></div>
</div>
<footer>{{site.footer}}</footer>
</div>

As you can see from the markup, we are calling in two directives: admin-login, which will
display a welcome message to the logged-in user, and nav-bar, which will show relevant
navigation links on the left-hand side of the window.
We also plan to have a scope object called site and are displaying the site logo and site
footer on this template.
The next step is to create our AppCtrl function in our controller, which is done as follows:
.controller('AppCtrl',
['$scope','AuthService','flashMessageService','$location',function($scope,A
uthService,flashMessageService,$location) {
$scope.site = {
logo: "img/angcms-logo.png",
footer: "Copyright 2014 Angular CMS"
};
}
])

28

Refresh the page and notice the logo and footer. Needless to say, ensure that you have a logo
named angcms-logo.png present in the img folder.

Building the admin-login directive


The next directive that well build is the admin login, which will display the Welcome
message and have additional links to jump to the admin or log out.

Building our navigation bar directive

<username>

We would like our navigation bar to display the links for all the pages created via the admin.
We would like these links to be displayed in a sequence based on their menuIndex values.

Lets add the following directive to the directives.js file:


.directive('adminLogin', [
function() {
return {
controller: function($scope, $cookies) {
$scope.loggedInUser = $cookies.loggedInUser;
},
templateUrl: 'partials/directives/admin-login.html'
};
}
]);

We would also like this directive to display the admin menu links when the user is in the
admin section.
With these goals in mind, lets create our directive in the directives.js file as follows:
directive('navBar', [
function() {
return {
controller: function($scope, pagesFactory, $location) {
var path = $location.path().substr(0, 6);
if (path == "/admin") {
$scope.navLinks = [{
title: 'Pages',
url: 'admin'
}, {
title: 'Site Settings',
url: 'admin/site-settings'
}, ];
} else {
pagesFactory.getPages().then(
function(response) {
$scope.navLinks = response.data;
}, function() {

The controller code is straightforward, and it simply assigns the loggedInUser value from
the cookie to the scope object.
We will create its template as a new file in partials/directives/admin-login.html as
follows:
<div ng-if=loggedInUser>
Welcome {{loggedInUser}} | <a href="admin/pages">My Admin</a> | <a
href ng-click='logout()'>Logout</a>
</div>

Next, we will quickly write the code for the logout method. As this directive is within the
scope of AppCtrl, we will write this method within the AppCtrl function as follows:

});
}

$scope.logout = function() {
AuthService.logout().then(
function() {

},
templateUrl: 'partials/directives/nav.html'
};

$location.path('/admin/login');
flashMessageService.setMessage("Successfully logged out");

}
])

What we are doing here is using $location.path, we are trying to see whether the user is in
the admin section or on the frontend, and based on this, we are populating the navLinks
scope object with the relevant menu links.

}, function(err) {
console.log('there was an error tying to logout');
});
};

Displaying the content of a page

Next, lets create the template for this directive. Create a new file named nav.html in
angcms/public/partials/directives/nav.html, and add the following code:

The last and most crucial step of this entire project is to display the actual content of the
selected page.

<ul class="nav-links">
<li ng-repeat="nav in navLinks | orderBy:'menuIndex'"> <a
href="/{{nav.url}}">{{nav.title}}</a>
</li>
</ul>

This will require us to create a new route that will accept route params. Lets get this done
first in our public/js/app.js file as follows:

As you see, we are using ng-repeat to list out our entire page menu and ordering it with the
help of menuIndex.

29

$routeProvider.when('/:url', {
templateUrl: 'partials/page.html',
controller: 'PageCtrl'
});

30

$scope.pageContent.content =
$sce.trustAsHtml(response.data.content);

Next, lets create the partials view as a new file called partials/page.html with the
following content:

}, function() {
console.log('error fetching data');

<h1>{{pageContent.title}}</h1>
<div ng-bind-html="pageContent.content"></div>

});
}])

We are using the ng-bind-html directive here so that the HTML content is rendered
correctly instead of it spitting out the raw HTML as it is.

Save the file and refresh any of the page URLs. Now, you should be able to see the title and
page contents with the HTML formatting.

Next, lets create our PageCtrl function in controllers.js as follows:

Setting the default home page


.controller('PageCtrl', [ '$scope','pagesFactory', '$routeParams ',
function($scope, pagesFactory, $routeParams) {
var url = $routeParams.url;
pagesFactory.getPageContent(url).then(
function(response) {
$scope.pageContent = response.data;
}, function() {
console.log('error fetching data');
});
}]);

Now, our public-facing frontend is working quite well with all the nav links, content, and so
on. However, when you launch the site for the first time or hit http://localhost:3000/,
we land up with a blank screen.
To overcome this, we will make sure that our site always has a page titled Home.
Then, in the page controller, we will simply add the following highlighted line, which will set
the default value of the URL to home in case we dont find a URL param in the current route;
we will add this to the PageCtrl function:

Save the file, refresh the site, and hit any of the frontend links. Youll get an error in your
console; you will see something like the following screenshot:

var url = $routeParams.url;


if(!url) url="home";

Now, the home page will load by default for the preceding URL link. Alternatively, you can
also set the $routeProvider redirect in the public/js/app.js file to, say, the following:
$routeProvider.otherwise({redirectTo: '/home'});

wrapping up

So, what went wrong here? What is $sce?


Note

One of the coolest things about testing AngularJS apps in Google Chrome is whenever there
is an error message, AngularJS has a hyperlink that will take you directly to the site that
explains what the error is.
By reading up on the link, youll get to know that the Strict Contextual Escaping (SCE) mode
of AngularJS is turned on by default, and AngularJS feels that the HTML markup on the
content of our CMS pages is unsafe. To overcome this, we will need to explicitly tell $sce to
trust our content. We do this in our controller by adding the following highlighted lines to the
PageCtrl function:
.controller('PageCtrl', ['$scope','pagesFactory', '$routeParams', '$sce',
function($scope, pagesFactory, $routeParams,$sce) {
var url = $routeParams.url;
pagesFactory.getPageContent(url).then(
function(response) {
$scope.pageContent = {};
$scope.pageContent.title = response.data.title;

31

We went full stack, right from coding our backend by building REST APIs to saving and
reading data from the database. We also built the AngularJS frontend that interacts with these
backend APIs.
The key takeaways from this Tutorial are as follows:

Building backend web services using Node.js, MongoDB, and ExpressJS


Securing API using sessions
Making AngularJS and ExpressJS work together and build routes that span across both the
systems
Authenticating on the client side using Interceptors
Integrating third-party modules
Using custom filters to format and store data
Building a custom module for a global notification system

32

You might also like