Create You Own CMS With Angularjs From Scratch
Create You Own CMS With Angularjs From Scratch
Create You Own CMS With Angularjs From Scratch
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/.
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.
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:
Once you have installed MongoDB, the next most important step is to create the folder to
store your data.
Create an empty folder named data/db on the root using the following command line:
mkdir /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
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
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:
The following table gives a description for the fields in the schema:
npm install
Description
Fields
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
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
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:
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.
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:
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.
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:
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:
Add the preceding two lines within the respective sections of the app.js file.
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
});
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:
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);
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.
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');
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:
});
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.
Next, we create the route for login. Add the following code to the api.js file:
7
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;
We then include the following lines in the respective sections of our app.js file:
var session = require('express-session');
app.use (session());
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:
}
});
});
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.
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.
We will be using this status code in our AngularJS side to redirect the users in case of session
time outs and so on.
});
});
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.
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.
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.
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) {
$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.
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.
'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 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.
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.
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:
<h1>Login</h1>
<hr/>
code:
<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>
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
}])
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.
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.
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:
<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:
angular.module('myApp', [
'ngRoute',
'myApp.filters',
'myApp.services',
'myApp.directives',
'myApp.controllers',
'ngCookies'
])
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:
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
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.
Tip
The developer console should, however, show a 401 Unauthorized failed message.
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');
}
};
}])
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:
.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>'
}
}
]);
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.
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
21
22
},
function() {
$log.error('error saving data');
}
);
};
}
])
},
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.
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.
<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:
<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.
'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.
};
}
]);
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:
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:
<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.
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:
<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.
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);
}
};
}]);
27
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.
<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.
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');
});
};
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.
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:
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
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:
32