全部博文(1159)
分类: Web开发
2016-03-16 22:37:30
In this chapter, we are going to build a blog application by using Node.js and AngularJS. Our system will support adding, editing, and removing articles, so there will be a control panel. The MongoDB or MySQL database will handle the storing of the information and the Express framework will be used as the site base. It will deliver the JavaScript, CSS, and the HTML to the end user, and will provide an API to access the database. We will use AngularJS to build the user interface and control the client-side logic in the administration page.
This chapter will cover the following topics:
AngularJS is an open source, client-side JavaScript framework developed by Google. It's full of features and is really well documented. It has almost become a standard framework in the development of single-page applications. The official site of AngularJS, , provides a well-structured documentation. As the framework is widely used, there is a lot of material in the form of articles and video tutorials. As a JavaScript library, it collaborates pretty well with Node.js. In this chapter, we will build a simple blog with a control panel.
Before we start developing our application, let's first take a look at the framework. AngularJS gives us very good control over the data on our page. We don't have to think about selecting elements from the DOM and filling them with values. Thankfully, due to the available data-binding, we may update the data in the JavaScript part and see the change in the HTML part. This is also true for the reverse. Once we change something in the HTML part, we get the new values in the JavaScript part. The framework has a powerful dependency injector. There are predefined classes in order to perform AJAX requests and manage routes.
You could also read Mastering Web Development with AngularJS by Peter Bacon Darwin and Pawel Kozlowski, published by Packt Publishing.
To bootstrap an AngularJS application, we need to add the ng-app attribute to some of our HTML tags. It is important that we pick the right one. Having ng-app somewhere means that all the child nodes will be processed by the framework. It's common practice to put that attribute on the tag. In the following code, we have a simple HTML page containing ng-app:
<html ng-app> <head> <script src="angular.min.js">script> head> <body> ... body> html>
Very often, we will apply a value to the attribute. This will be a module name. We will do this while developing the control panel of our blog application. Having the freedom to place ng-app wherever we want means that we can decide which part of our markup will be controlled by AngularJS. That's good, because if we have a giant HTML file, we really don't want to spend resources parsing the whole document. Of course, we may bootstrap our logic manually, and this is needed when we have more than one AngularJS application on the page.
In AngularJS, we can implement the Model-View-Controller pattern. The controller acts as glue between the data (model) and the user interface (view). In the context of the framework, the controller is just a simple function. For example, the following HTML code illustrates that a controller is just a simple function:
<html ng-app> <head> <script src="angular.min.js">script> <script src="HeaderController.js">script> head> <body> <header ng-controller="HeaderController"> <h1>{{title}}h1> header> body> html>
In
of the page, we are adding the minified version of the library and HeaderController.js; a file that will host the code of our controller. We also set an ng-controller attribute in the HTML markup. The definition of the controller is as follows:function HeaderController($scope) { $scope.title = "Hello world";
}
Every controller has its own area of influence. That area is called the scope. In our case, HeaderController defines the {{title}} variable. AngularJS has a wonderful dependency-injection system. Thankfully, due to this mechanism, the $scope argument is automatically initialized and passed to our function. The ng-controller attribute is called the directive, that is, an attribute, which has meaning to AngularJS. There are a lot of directives that we can use. That's maybe one of the strongest points of the framework. We can implement complex logic directly inside our templates, for example, data binding, filtering, or modularity.
Data binding is a process of automatically updating the view once the model is changed. As we mentioned earlier, we can change a variable in the JavaScript part of the application and the HTML part will be automatically updated. We don't have to create a reference to a DOM element or attach event listeners. Everything is handled by the framework. Let's continue and elaborate on the previous example, as follows:
<header ng-controller="HeaderController"> <h1>{{title}}h1> <a href="#" ng-click="updateTitle()">change titlea> header>
A link is added and it contains the ng-click directive. The updateTitle function is a function defined in the controller, as seen in the following code snippet:
function HeaderController($scope) { $scope.title = "Hello world"; $scope.updateTitle = function() { $scope.title = "That's a new title.";
}
}
We don't care about the DOM element and where the {{title}} variable is. We just change a property of $scope and everything works. There are, of course, situations where we will have the fields and we want to bind their values. If that's the case, then the ng-model directive can be used. We can see this as follows:
<header ng-controller="HeaderController"> <h1>{{title}}h1> <a href="#" ng-click="updateTitle()">change titlea> <input type="text" ng-model="title" /> header>
The data in the input field is bound to the same title variable. This time, we don't have to edit the controller. AngularJS automatically changes the content of the h1 tag.
It's great that we have controllers. However, it's not a good practice to place everything into globally defined functions. That's why it is good to use the module system. The following code shows how a module is defined:
angular.module('HeaderModule', []);
The first parameter is the name of the module and the second one is an array with the module's dependencies. By dependencies, we mean other modules, services, or something custom that we can use inside the module. It should also be set as a value of the ng-app directive. The code so far could be translated to the following code snippet:
angular.module('HeaderModule', [])
.controller('HeaderController', function($scope) { $scope.title = "Hello world"; $scope.updateTitle = function() { $scope.title = "That's a new title.";
}
});
So, the first line defines a module. We can chain the different methods of the module and one of them is the controller method. Following this approach, that is, putting our code inside a module, we will be encapsulating logic. This is a sign of good architecture. And of course, with a module, we have access to different features such as filters, custom directives, and custom services.
The filters are very handy when we want to prepare our data, prior to be displayed to the user. Let's say, for example, that we need to mention our title in uppercase once it reaches a length of more than 20 characters:
angular.module('HeaderModule', [])
.filter('customuppercase', function() { return function(input) { if(input.length > 20) { return input.toUpperCase();
} else { return input;
}
};
})
.controller('HeaderController', function($scope) { $scope.title = "Hello world"; $scope.updateTitle = function() { $scope.title = "That's a new title.";
}
});
That's the definition of the custom filter called customuppercase. It receives the input and performs a simple check. What it returns, is what the user sees at the end. Here is how this filter could be used in HTML:
<h1>{{title | customuppercase}}h1>
Of course, we may add more than one filter per variable. There are some predefined filters to limit the length, such as the JavaScript to JSON conversion or, for example, date formatting.
Dependency management can be very tough sometimes. We may split everything into different modules/components. They have nicely written APIs and they are very well documented. However, very soon, we may realize that we need to create a lot of objects. Dependency injection solves this problem by providing what we need, on the fly. We already saw this in action. The $scope parameter passed to our controller, is actually created by the injector of AngularJS. To get something as a dependency, we need to define it somewhere and let the framework know about it. We do this as follows:
angular.module('HeaderModule', [])
.factory("Data", function() { return {
getTitle: function() { return "A better title.";
}
}
})
.controller('HeaderController', function($scope, Data) { $scope.title = Data.getTitle(); $scope.updateTitle = function() { $scope.title = "That's a new title.";
}
});
The Module class has a method called factory. It registers a new service that could later be used as a dependency. The function returns an object with only one method, getTitle. Of course, the name of the service should match the name of the controller's parameter. Otherwise, AngularJS will not be able to find the dependency's source.
In the well known Model-View-Controller pattern, the model is the part that stores the data in the application. AngularJS doesn't have a specific workflow to define models. The $scope variable could be considered a model. We keep the data in properties attached to the current scope. Later, we can use the ng-model directive and bind a property to the DOM element. We already saw how this works in the previous sections. The framework may not provide the usual form of a model, but it's made like that so that we can write our own implementation. The fact that AngularJS works with plain JavaScript objects, makes this task easily doable.
AngularJS is one of the leading frameworks, not only because it is made by Google, but also because it's really flexible. We could use just a small piece of it or build a solid architecture using the giant collection of features.
To build a blog application, we need a database that will store the published articles. In most cases, the choice of the database depends on the current project. There are factors such as performance and scalability and we should keep them in mind. In order to have a better look at the possible solutions, we will have a look at the two of the most popular databases: MongoDB and MySQL. The first one is a NoSQL type of database. According to the Wikipedia entry () on NoSQL databases:
"A NoSQL or Not Only SQL database provides a mechanism for storage and retrieval of data that is modeled in means other than the tabular relations used in relational databases."
In other words, it's simpler than a SQL database, and very often stores information in the key value type. Usually, such solutions are used when handling and storing large amounts of data. It is also a very popular approach when we need flexible schema or when we want to use JSON. It really depends on what kind of system we are building. In some cases, MySQL could be a better choice, while in some other cases, MongoDB. In our example blog, we're going to use both.
In order to do this, we will need a layer that connects to the database server and accepts queries. To make things a bit more interesting, we will create a module that has only one API, but can switch between the two database models.
Let's start with MongoDB. Before we start storing information, we need a MongoDB server running. It can be downloaded from the official page of the database .
We are not going to handle the communication with the database manually. There is a driver specifically developed for Node.js. It's called mongodb and we should include it in our package.json file. After successful installation via npm install, the driver will be available in our scripts. We can check this as follows:
"dependencies": { "mongodb": "1.3.20" }
We will stick to the Model-View-Controller architecture and the database-related operations in a model called Articles. We can see this as follows:
var crypto = require("crypto"),
type = "mongodb",
client = require('mongodb').MongoClient,
mongodb_host = "127.0.0.1",
mongodb_port = "27017",
collection; module.exports = function() { if(type == "mongodb") { return {
add: function(data, callback) { ... },
update: function(data, callback) { ... },
get: function(callback) { ... },
remove: function(id, callback) { ... }
}
} else { return {
add: function(data, callback) { ... },
update: function(data, callback) { ... },
get: function(callback) { ... },
remove: function(id, callback) { ... }
}
}
}
It starts with defining a few dependencies and settings for the MongoDB connection. Line number one requires the crypto module. We will use it to generate unique IDs for every article. The type variable defines which database is currently accessed. The third line initializes the MongoDB driver. We will use it to communicate with the database server. After that, we set the host and port for the connection and at the end a global collection variable, which will keep a reference to the collection with the articles. In MongoDB, the collections are similar to the tables in MySQL. The next logical step is to establish a database connection and perform the needed operations, as follows:
connection = 'mongodb://';
connection += mongodb_host + ':' + mongodb_port;
connection += '/blog-application';
client.connect(connection, function(err, database) { if(err) { throw new Error("Can't connect");
} else { console.log("Connection to MongoDB server successful.");
collection = database.collection('articles');
}
});
We pass the host and the port, and the driver is doing everything else. Of course, it is a good practice to handle the error (if any) and throw an exception. In our case, this is especially needed because without the information in the database, the frontend has nothing to show. The rest of the module contains methods to add, edit, retrieve, and delete records:
return {
add: function(data, callback) { var date = new Date();
data.id = crypto.randomBytes(20).toString('hex');
data.date = date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate();
collection.insert(data, {}, callback || function() {});
},
update: function(data, callback) {
collection.update(
{ID: data.id},
data,
{},
callback || function(){ }
);
},
get: function(callback) {
collection.find({}).toArray(callback);
},
remove: function(id, callback) {
collection.findAndModify(
{ID: id},
[],
{},
{remove: true},
callback
);
}
}
The add and update methods accept the data parameter. That's a simple JavaScript object. For example, see the following code:
{ title: "Blog post title",
text: "Article's text here ..." }
The records are identified by an automatically generated unique id. The update method needs it in order to find out which record to edit. All the methods also have a callback. That's important, because the module is meant to be used as a black box, that is, we should be able to create an instance of it, operate with the data, and at the end continue with the rest of the application's logic.
We're going to use an SQL type of database with MySQL. We will add a few more lines of code to the already working Articles.js model. The idea is to have a class that supports the two databases like two different options. At the end, we should be able to switch from one to the other, by simply changing the value of a variable. Similar to MongoDB, we need to first install the database to be able use it. The official download page is .
MySQL requires another Node.js module. It should be added again to the package.json file. We can see the module as follows:
"dependencies": { "mongodb": "1.3.20", "mysql": "2.0.0" }
Similar to the MongoDB solution, we need to firstly connect to the server. To do so, we need to know the values of the host, username, and password fields. And because the data is organized in databases, a name of the database. In MySQL, we put our data into different databases. So, the following code defines the needed variables:
var mysql = require('mysql'),
mysql_host = "127.0.0.1",
mysql_user = "root",
mysql_password = "",
mysql_database = "blog_application",
connection;
The previous example leaves the password field empty but we should set the proper value of our system. The MySQL database requires us to define a table and its fields before we start saving data. So, the following code is a short dump of the table used in this chapter:
CREATE TABLE IF NOT EXISTS `articles` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` longtext NOT NULL, `text` longtext NOT NULL, `date` varchar(100) NOT NULL, PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
Once we have a database and its table set, we can continue with the database connection, as follows:
connection = mysql.createConnection({
host: mysql_host,
user: mysql_user,
password: mysql_password
});
connection.connect(function(err) { if(err) { throw new Error("Can't connect to MySQL.");
} else {
connection.query("USE " + mysql_database, function(err, rows, fields) { if(err) { throw new Error("Missing database.");
} else { console.log("Successfully selected database.");
}
})
}
});
The driver provides a method to connect to the server and execute queries. The first executed query selects the database. If everything is ok, you should see Successfully selected database as an output in your console. Half of the job is done. What we should do now is replicate the methods returned in the first MongoDB implementation. We need to do this because when we switch to the MySQL usage, the code using the class will not work. And by replicating them we mean that they should have the same names and should accept the same arguments.
If we do everything correctly, at the end our application will support two types of databases. And all we have to do is change the value of the type variable:
return {
add: function(data, callback) { var date = new Date(); var query = "";
query += "INSERT INTO articles (title, text, date) VALUES (";
query += connection.escape(data.title) + ", ";
query += connection.escape(data.text) + ", ";
query += "'" + date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate() + "'";
query += ")";
connection.query(query, callback);
},
update: function(data, callback) { var query = "UPDATE articles SET ";
query += "title=" + connection.escape(data.title) + ", ";
query += "text=" + connection.escape(data.text) + " ";
query += "WHERE id='" + data.id + "'";
connection.query(query, callback);
},
get: function(callback) { var query = "SELECT * FROM articles ORDER BY id DESC";
connection.query(query, function(err, rows, fields) { if(err) { throw new Error("Error getting.");
} else {
callback(rows);
}
});
},
remove: function(id, callback) { var query = "DELETE FROM articles WHERE id='" + id + "'";
connection.query(query, callback);
}
}
The code is a little longer than the one generated in the first MongoDB variant. That's because we needed to construct MySQL queries from the passed data. Keep in mind that we have to escape the information, which comes to the module. That's why we use connection.escape(). With these lines of code, our model is completed. Now we can add, edit, remove, or get data. Let's continue with the part that shows the articles to our users.
Let's assume that there is some data in the database and we are ready to present it to the users. So far, we have only developed the model, which is the class that takes care of the access to the information. In the previous chapter of this book, we learned about Express. To simplify the process, we will use it again here. We need to first update the package.json file and include that in the framework, as follows:
"dependencies": { "express": "3.4.6", "jade": "0.35.0", "mongodb": "1.3.20", "mysql": "2.0.0" }
We are also adding Jade, because we are going to use it as a template language. The writing of markup in plain HTML is not very efficient nowadays. By using the template engine, we can split the data and the HTML markup, which makes our application much better structured. Jade's syntax is kind of similar to HTML. We can write tags without the need to close them:
body p(class="paragraph", data-id="12").
Sample text here
footer a(href="#").
my site
The preceding code snippet is transformed to the following code snippet:
<body> <p data-id="12" class="paragraph">Sample text herep> <footer><a href="#">my sitea>footer> body>
Jade relies on the indentation in the content to distinguish the tags.
Let's start with the project structure, as seen in the following screenshot:
We placed our already written class, Articles.js, inside the models directory. The public directory will contain CSS styles, and all the necessary client-side JavaScript: the AngularJS library, the AngularJS router module, and our custom code.
We will skip some of the explanations about the following code, because we already covered that in the previous chapter. Our index.js file looks as follows:
var express = require('express'); var app = express(); var articles = require("./models/Articles")();
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.static(__dirname + '/public'));
app.use(function(req, res, next) {
req.articles = articles;
next();
});
app.get('/api/get', require("./controllers/api/get"));
app.get('/', require("./controllers/index"));
app.listen(3000);
console.log('Listening on port 3000');
At the beginning, we require the Express framework and our model. Maybe it's better to initialize the model inside the controller, but in our case this is not necessary. Just after that, we set up some basic options for Express and define our own middleware. It has only one job to do and that is to attach the model to the request object. We are doing this because the request object is passed to all the route handlers. In our case, these handlers are actually the controllers. So, Articles.js becomes accessible everywhere via the req.articles property. At the end of the script, we placed two routes. The second one catches the usual requests that come from the users. The first one, /api/get, is a bit more interesting. We want to build our frontend on top of AngularJS. So, the data that is stored in the database should not enter the Node.js part but on the client side where we use Google's framework. To make this possible, we will create routes/controllers to get, add, edit, and delete records. Everything will be controlled by HTTP requests performed by AngularJS. In other words, we need an API.
Before we start using Angular, let's take a look at the /controllers/api/get.js controller:
module.exports = function(req, res, next) {
req.articles.get(function(rows) {
res.send(rows);
});
}
The main job is done by our model and the response is handled by Express. It's nice because if we pass a JavaScript object, as we did, (rows is actually an array of objects) the framework sets the response headers automatically. To test the result, we could run the application with node index.js and open If we don't have any records in the database, we will get an empty array. If not, the stored articles will be returned. So, that's the URL, which we should hit from within the AngularJS controller in order to get the information.
The code of the /controller/index.js controller is also just a few lines. We can see the code as follows:
module.exports = function(req, res, next) {
res.render("list", { app: "" });
}
It simply renders the list view, which is stored in the list.jade file. That file should be saved in the /views directory. But before we see its code, we will check another file, which acts as a base for all the pages. Jade has a nice feature called blocks. We may define different partials and combine them into one template. The following is our layout.jade file:
doctype html html(ng-app="#{app}") head
title Blog link(rel='stylesheet', href='/style.css') script(src='/angular.min.js') script(src='/angular-route.min.js') body
block content
There is only one variable passed to this template, which is #{app}. We will need it later to initialize the administration's module. The angular.min.js and angular-route.min.js files should be downloaded from the official AngularJS site, and placed in the /public directory. The body of the page contains a block placeholder called content, which we will later fill with the list of the articles. The following is the list.jade file:
extends layout
block content
.container(ng-controller="BlogCtrl")
section.articles article(ng-repeat="article in articles") h2 {{article.title}}
br
small published on {{article.date}}
p {{article.text}}
script(src='/blog.js')
The two lines in the beginning combine both the templates into one page. The Express framework transforms the Jade template into HTML and serves it to the browser of the user. From there, the client-side JavaScript takes control. We are using the ng-controller directive saying that the div element will be controlled by an AngularJS controller called BlogCtrl. The same class should have variable, articles, filled with the information from the database. ng-repeat goes through the array and displays the content to the users. The blog.js class holds the code of the controller:
function BlogCtrl($scope, $http) { $scope.articles = [
{ title: "", text: "Loading ..."}
]; $http({method: 'GET', url: '/api/get'})
.success(function(data, status, headers, config) { $scope.articles = data;
})
.error(function(data, status, headers, config) {
console.error("Error getting articles.");
});
}
The controller has two dependencies. The first one, $scope, points to the current view. Whatever we assign as a property there is available as a variable in our HTML markup. Initially, we add only one element, which doesn't have a title, but has text. It is shown to indicate that we are still loading the articles from the database. The second dependency, $http, provides an API in order to make HTTP requests. So, all we have to do is query /api/get, fetch the data, and pass it to the $scope dependency. The rest is done by AngularJS and its magical two-way data binding. To make the application a little more interesting, we will add a search field, as follows:
// views/list.jade header
.search input(type="text", placeholder="type a filter here", ng-model="filterText") h1 Blog
hr
The ng-model directive, binds the value of the input field to a variable inside our $scope dependency. However, this time, we don't have to edit our controller and can simply apply the same variable as a filter to the ng-repeat:
article(ng-repeat="article in articles | filter:filterText")
As a result, the articles shown will be filtered based on the user's input. Two simple additions, but something really valuable is on the page. The filters of AngularJS can be very powerful.
The control panel is the place where we will manage the articles of the blog. Several things should be made in the backend before continuing with the user interface. They are as follows:
app.set("username", "admin");
app.set("password", "pass");
app.use(express.cookieParser('blog-application'));
app.use(express.session());
The previous lines of code should be added to /index.js. Our administration should be protected, so the first two lines define our credentials. We are using Express as data storage, simply creating key-value pairs. Later, if we need the username we can get it with app.get("username"). The next two lines enable session support. We need that because of the login process.
We added a middleware, which attaches the articles to the request object. We will do the same with the current user's status, as follows:
app.use(function(req, res, next) { if((
req.session &&
req.session.admin === true ) || (
req.body &&
req.body.username === app.get("username") &&
req.body.password === app.get("password")
)) {
req.logged = true;
req.session.admin = true;
};
next();
});
Our if statement is a little long, but it tells us whether the user is logged in or not. The first part checks whether there is a session created and the second one checks whether the user submitted a form with the correct username and password. If these expressions are true, then we attach a variable, logged, to the request object and create a session that will be valid during the following requests.
There is only one thing that we need in the main application's file. A few routes that will handle the control panel operations. In the following code, we are defining them along with the needed route handler:
var protect = function(req, res, next) { if(req.logged) {
next();
} else {
res.send(401, 'No Access.');
}
}
app.post('/api/add', protect, require("./controllers/api/add"));
app.post('/api/edit', protect, require("./controllers/api/edit"));
app.post('/api/delete', protect , require("./controllers/api/delete"));
app.all('/admin', require("./controllers/admin"));
The three routes, which start with /api, will use the model Articles.js to add, edit, and remove articles from the database. These operations should be protected. We will add a middleware function that takes care of this. If the req.logged variable is not available, it simply responds with a 401 - Unauthorized status code. The last route, /admin, is a little different because it shows a login form instead. The following is the controller to create new articles:
module.exports = function(req, res, next) {
req.articles.add(req.body, function() {
res.send({success: true});
});
}
We transfer most of the logic to the frontend, so again, there are just a few lines. What is interesting here is that we pass req.body directly to the model. It actually contains the data submitted by the user. The following code, is how the req.articles.add method looks for the MongoDB implementation:
add: function(data, callback) {
data.ID = crypto.randomBytes(20).toString('hex');
collection.insert(data, {}, callback || function() {});
}
And the MySQL implementation is as follows:
add: function(data, callback) { var date = new Date(); var query = "";
query += "INSERT INTO articles (title, text, date) VALUES (";
query += connection.escape(data.title) + ", ";
query += connection.escape(data.text) + ", ";
query += "'" + date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate() + "'";
query += ")";
connection.query(query, callback);
}
In both the cases, we need title and text in the passed data object. Thankfully, due to Express' bodyParser middleware, this is what we have in the req.body object. We can directly forward it to the model. The other route handlers are almost the same:
// api/edit.js module.exports = function(req, res, next) {
req.articles.update(req.body, function() {
res.send({success: true});
});
}
What we changed is the method of the Articles.js class. It is not add but update. The same technique is applied in the route to delete an article. We can see it as follows:
// api/delete.js module.exports = function(req, res, next) {
req.articles.remove(req.body.id, function() {
res.send({success: true});
});
}
What we need for deletion is not the whole body of the request but only the unique ID of the record. Every API method sends {success: true} as a response. While we are dealing with API requests, we should always return a response. Even if something goes wrong.
The last thing in the Node.js part, which we have to cover, is the controller responsible for the user interface of the administration panel, that is, the. /controllers/admin.js file:
module.exports = function(req, res, next) { if(req.logged) {
res.render("admin", { app: "admin" });
} else {
res.render("login", { app: "" });
}
}
There are two templates that are rendered: /views/admin.jade and /views/login.jade. Based on the variable, which we set in /index.js, the script decides which one to show. If the user is not logged in, then a login form is sent to the browser, as follows:
extends layout
block content
.container
header
h1 Administration
hr
section.articles
article form(method="post", action="/admin") span Username:
br input(type="text", name="username") br
span Password:
br input(type="password", name="password") br
br input(type="submit", value="login")
There is no AngularJS code here. All we have is the good old HTML form, which submits its data via POST to the same URL—/admin. If the username and password are correct, the .logged variable is set to true and the controller renders the other template:
extends layout
block content
.container
header
h1 Administration
hr a(href="/") Public
span | a(href="#/") List
span | a(href="#/add") Add section(ng-view) script(src='/admin.js')
The control panel needs several views to handle all the operations. AngularJS has a great router module, which works with hashtags-type URLs, that is, URLs such as /admin#/add. The same module requires a placeholder for the different partials. In our case, this is a section tag. The ng-view attribute tells the framework that this is the element prepared for that logic. At the end of the template, we are adding an external file, which keeps the whole client-side JavaScript code that is needed by the control panel.
While the client-side part of the applications needs only loading of the articles, the control panel requires a lot more functionalities. It is good to use the modular system of AngularJS. We need the routes and views to change, so the ngRoute module is needed as a dependency. This module is not added in the main angular.min.js build. It is placed in the angular-route.min.js file. The following code shows how our module starts:
var admin = angular.module('admin', ['ngRoute']);
admin.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {})
.when('/add', {})
.when('/edit/:id', {})
.when('/delete/:id', {})
.otherwise({ redirectTo: '/' });
}
]);
We configured the router by mapping URLs to specific routes. At the moment, the routes are just empty objects, but we will fix that shortly. Every controller will need to make HTTP requests to the Node.js part of the application. It will be nice if we have such a service and use it all over our code. We can see an example as follows:
admin.factory('API', function($http) { var request = function(method, url) { return function(callback, data) { $http({method: method, url: url, data: data})
.success(callback)
.error(function(data, status, headers, config) {
console.error("Error requesting '" + url + "'.");
});
}
} return {
get: request('GET', '/api/get'),
add: request('POST', '/api/add'),
edit: request('POST', '/api/edit'),
remove: request('POST', '/api/delete')
}
});
One of the best things about AngularJS is that it works with plain JavaScript objects. There are no unnecessary abstractions and no extending or inheriting special classes. We are using the .factory method to create a simple JavaScript object. It has four methods that can be called: get, add, edit, and remove. Each one of them calls a function, which is defined in the helper method request. The service has only one dependency, $http. We already know this module; it handles HTTP requests nicely. The URLs that we are going to query are the same ones that we defined in the Node.js part.
Now, let's create a controller that will show the articles currently stored in the database. First, we should replace the empty route object .when('/', {}) with the following object:
.when('/', {
controller: 'ListCtrl',
template: '\ <article ng-repeat="article in articles">\ <hr />\ <strong>{{article.title}}strong><br />\
(<a href="#/edit/{{article.id}}">edita>)\
(<a href="#/delete/{{article.id}}">removea>)\ article>\
'
})
The object has to contain a controller and a template. The template is nothing more than a few lines of HTML markup. It looks a bit like the template used to show the articles on the client side. The difference is the links used to edit and delete. JavaScript doesn't allow new lines in the string definitions. The backward slashes at the end of the lines prevent syntax errors, which will eventually be thrown by the browser. The following is the code for the controller. It is defined, again, in the module:
admin.controller('ListCtrl', function($scope, API) {
API.get(function(articles) { $scope.articles = articles;
});
});
And here is the beauty of the AngularJS dependency injection. Our custom-defined service API is automatically initialized and passed to the controller. The .get method fetches the articles from the database. Later, we send the information to the current $scope dependency and the two-way data binding does the rest. The articles are shown on the page.
The work with AngularJS is so easy that we could combine the controller to add and edit in one place. Let's store the route object in an external variable, as follows:
var AddEditRoute = {
controller: 'AddEditCtrl',
template: '\ <hr />\ <article>\ <form>\ <span>Titlespna><br />\ <input type="text" ng-model="article.title"/><br />\ <span>Textspna><br />\ <textarea rows="7" ng-model="article.text">textarea>\ <br /><br />\ <button ng-click="save()">savebutton>\ form>\ article>\
'
};
And later, assign it to the both the routes, as follows:
.when('/add', AddEditRoute)
.when('/edit/:id', AddEditRoute)
The template is just a form with the necessary fields and a button, which calls the save method in the controller. Notice that we bound the input field and the text area to variables inside the $scope dependency. This comes in handy because we don't need to access the DOM to get the values. We can see this as follows:
admin.controller( 'AddEditCtrl', function($scope, API, $location, $routeParams) { var editMode = $routeParams.id ? true : false; if(editMode) {
API.get(function(articles) {
articles.forEach(function(article) { if(article.id == $routeParams.id) { $scope.article = article;
}
});
});
} $scope.save = function() {
API[editMode ? 'edit' : 'add'](function() { $location.path('/');
}, $scope.article);
}
})
The controller receives four dependencies. We already know about $scope and API. The $location dependency is used when we want to change the current route, or, in other words, to forward the user to another view. The $routeParams dependency is needed to fetch parameters from the URL. In our case, /edit/:id is a route with a variable inside. Inside the code, the id is available in $routeParams.id. The adding and editing of articles uses the same form. So, with a simple check, we know what the user is currently doing. If the user is in the edit mode, then we fetch the article based on the provided id and fill the form. Otherwise, the fields are empty and new records will be created.
The deletion of an article can be done by using a similar approach, which is adding a route object and defining a new controller. We can see the deletion as follows:
.when('/delete/:id', { controller: 'RemoveCtrl', template: ' ' })
We don't need a template in this case. Once the article is deleted from the database, we will forward the user to the list page. We have to call the remove method of the API. Here is how the RemoveCtrl controller looks like:
admin.controller( 'RemoveCtrl', function($scope, $location, $routeParams, API) {
API.remove(function() { $location.path('/');
}, $routeParams);
}
);
The preceding code depicts same dependencies like in the previous controller. This time, we simply forward the $routeParams dependency to the API. And because it is a plain JavaScript object, everything works as expected.
In this chapter, we built a simple blog by writing the backend of the application in Node.js. The module for database communication, which we wrote, can work with the MongoDB or MySQL database and store articles. The client-side part and the control panel of the blog were developed with AngularJS. We then defined a custom service using the built-in HTTP and routing mechanisms.
Node.js works well with AngularJS, mainly because both are written in JavaScript. We found out that AngularJS is built to support the developer. It removes all those boring tasks such as DOM element referencing, attaching event listeners, and so on. It's a great choice for the modern client-side coding stack.
In the next chapter, we will see how to program a real-time chat with Socket.IO, one of the popular solutions covering the WebSockets communication.