Contact Manager – written in AngularJS, Express and MongoDB – Episode 2

In a previous post I have discussed how to create a RESTful API using express and MongoDB, and now, it's time to talk about how to create a frontend that connects to our API, retrieves and displays data. My choice fell on AngularJS - I wanted to explore this technology further and a bit more in detail after my previous post about it.

If you read the previous article, you may remember that the RESTul API lives on port 1222 - our frontend application will use port 1223 to communicate. Now obviously this setup is okay in a test environment, I only run one development virtual machine and all my code lives on the same box. Even though using different ports sounds very straight forward for testing and development, it's not as easy as it may seem first - I will elaborate on this a bit more later on.

To start off, we'll need something to serve the content of our .html file(s) - my choice fell on Express again.

Please create a file called server.js and place the following content inside:

var express = require("express");
var app = express();
app.configure(function(){
  app.use(express.static(__dirname + '/'));
});
app.listen(1223, "host");
console.log("Server is up and listening on port 1223");

If you create an index.html file with whatever content and place it to the same working directory, upon executing node server.js and pointing your web browser to http://host:1223, you should be able to see your HTML page generated. That's it, you now have your server up and running. For more information please check out the official Express guide.
(Please note that I'm aware that you can generate a folder structure automatically with Express, using the express [opts] [app-name] syntax, however for simplicity I'd like to keep my folder/file structure clean)

It's time to get serious and get some data retrieved. If you haven't already, please make sure that the API created in the previous post is up and running via the server (node index.js and it should be accessible via http://host:1222). As with all of my project I'm going to be using the Twitter Bootstrap library, and here's a basic framework for the app - I have picked a table to display the contacts. (Make sure to download the appropriate .js and .css files and/or link to them - i.e. via Google CDN)

<!DOCTYPE html>
<html>
  <head>
    <title>Contact manager</title>
    <link href="bootstrap.css" rel="stylesheet" media="screen">
    <script src="angular.min.js"></script>
  </head>
  <body>
    <h1>Contact manager</h1>
      <div>
        <table class="table table-hover">
            <thead>
              <tr>
                <th>
                  Name
                </th>
                <th>
                  Phone
                </th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>Contact 1</td>
                <td>1234567</td>
              </tr>
              <tr>
                <td>Contact 2</td>
                <td>123999000</td>
              </tr>
            </tbody>
        </table>
       </div>
    <script src="jquery.js"></script>
    <script src="bootstrap.js"></script>
  </body>
</html>

(You can of course drop the any of the .js/.css files other than angular.js if you don't require them - I use them in my projects usually)

This should result in the following:
Contact Manager HTML basic frame

I'm sure you've noticed that this is fairly static, let's add some AngularJS magic to it. First, we need to declare our controller and scope. The controller that will be responsible to list all our contacts is cleverly called ListController. For demonstration purposes I'm going to manually create a JSON object with contact names and phone numbers first.

I'm not going to go into the detail of how to declare the scope of our AngularJS application in this post, but feel free to have a look at their official documentation or my previous post about this topic.

There are a few things that we need to modify in the HTML skeleton above. I'd like to have my header(s) automatically populated as well as my data. In order to achieve what we want, we need to create a JavaScript file - for simplicity I'm going to use inline scripts:

angular.module('contactmanager', []);
function ListController($scope) {
  $scope.headers = ["name", "phone"];
  $scope.contacts = [
    { "name" : "Tamas Piros", "phone" : "123456" },
    { "name" : "Uncle Joe", "phone" : "088882" },
  ];
}

And modify HTML portion to look like this:

<!DOCTYPE html>
<html ng-app="contactmanager">
  <head>
    <title>Contact manager</title>
    <link href="bootstrap.css" rel="stylesheet" media="screen">
    <script src="angular.min.js"></script>
    <script><!-- Insert the JavaScript from above here --></script>
  </head>
  <body ng-controller="ListController">
    <h1>Contact manager</h1>
      <div>
        <table class="table table-hover">
            <thead>
              <tr>
                <th ng-repeat="header in headers">{{ header }}
              </tr>
            </thead>
            <tbody>
              <tr ng-repeat="contact in contacts">
                <td>{{ contact.name }}</td>
                <td>{{ contact.phone }}</td>
              </tr>
            </tbody>
        </table>
       </div>
    <script src="jquery.js"></script>
    <script src="bootstrap.js"></script>
  </body>
</html>

We register the contactmanager module and create the ListController controller that will add the headers and the contacts to our application. We then - in the HTML table - iterate through these JSON objects and print out the necessary information. This is great, however it's still very static, it's time to replace the contacts JSON object with information from our Mongo database.

There are two ways of achieving this. If you recall I mentioned that I'm using the same host for the backend and the frontend server as well, the difference is that they run on different ports - this means that I cannot make simple "GET" requests to the API, because they will fail with a "Access-Control-Allow-Origin" error. (To read more information about this, please read this very interesting article.) In order to overcome the issue and still be able to do testing on a single server environment we need to introduce a concept called JSONP - instead of making a GET request to our API that'd result in a JSON object, we are going to make a JSONP call that returns - yeah, you guessed right, a JSONP object.

AngularJS is capable of making HTTP calls via a built in $http service. This also means that we need to rework our JavaScript slightly:

function ListController($scope, $http) {
  $scope.headers = ["name", "phone"];
  $http({method: 'jsonp', url: 'http://host:1222/contacts?callback=JSON_CALLBACK'}).success(function(data, status, headers, config) {
    $scope.contacts = data;
  }).
  error(function(data, status, headers, config) {
    //handle error
  });
}

And we also need to modify the index method created in the first part of this article to return a JSONP object as opposed to a JSON object:

exports.index = function (req, res){
  return ContactModel.find(function (err, contacts) {
    if (!err) {
      res.jsonp(contacts);
    } else {
      console.log(err);
    }
  });
}

(Please update all functions to return jsonp.)

And there you have it. Refreshing the frontend interface (http://host:1223/) should list you all the contacts that are stored in the Mongo database.

Let's add a few more features. No contact manager should exist without a search/filter function - adding this via AngularJS is relatively easy but I would also like to add a few more 'nice-to-have' features such as sorting of columns and capitalisation of the table headers. We need to make further modifications to ListController:

angular.module("filters", []).filter('capitalise', function() {
  return function(input) {
  if(input != null)
    return input.substring(0,1).toUpperCase() + input.substring(1);
  }
});

angular.module('contactmanager', ['filters']);

To capitalise the table headers, I created a custom filter. It's very important to register this filter when declaring contactmanager. You may wonder why am I writing a function to capitalise the headers and why not only create the $scope.headers with capitalised names. Well, it's simple - capitalised values in the JSON object would mean that I would have to have the same capitalisation going on in my database (i.e. the database "rows" would have to have capitalised names) and I don't like that concept. That's one thing being taken care of. Let's add the table heading sorting, capitalisation and querying as well - update the table in your index.html accordingly:

<ul class="inline">
    <li>You have {{contacts.length}} contacts.</li>
    <li>
      <div class="input-prepend">
        <span class="add-on"><i class="icon-search"></i></span>
        <input class="span2" id="inputIcon" type="text" placeholder="Search..." ng-model="query">
      </div>
   </li>
   <li>
     <p>Sorted by: {{columnSort.sortColumn}}</p>
   </li>
  </ul>
  <table class="table table-hover">
      <thead>
        <tr>
          <th ng-repeat="header in headers">
            <a ng-click="columnSort.sortColumn=headers[$index];columnSort.reverse=!columnSort.reverse">{{ headers[$index] | capitalise }}</a>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr ng-repeat="contact in contacts | orderBy:columnSort.sortColumn:columnSort.reverse | filter: query">
          <td>{{ contact.name }}</td>
          <td>{{ contact.phone }}</td>
        </tr>
      </tbody>
  </table>

The final result should look similar to this:
Final Contact Manager

I've originally planned on creating two articles but I'll have to write one more, in which I will explain the routing from AngularJS and calling the update/delete functions as well. Stay tuned.

Show Comments