Chapter 5. AngularJS Views and Bootstrap

We will now start a new AngularJS blog project that uses public REST services created especially for this book. We will work on the blog project for the rest of this book. You can also download the project code from GitHub. We will start off by building the views and the controllers for those views.

Twitter Bootstrap is a free collection of HTML and CSS templates. We will build the AngularJS views with the help of Twitter Bootstrap to help cut development time. Once we have the views and controllers in place and understand their operation, we will focus on the model and REST services (in the next two chapters).

Adding a New Blog Controller

Next we will set up the controllers for our new blog application. The following code defines the blogControllers module and the BlogCtrl controller for that module. We will define more controllers on the blogControllers module as we work on the blog application. For now, the controllers.js file is relatively small:

/* chapter5/controllers.js */

'use strict';
/* Controllers */

var blogControllers = 
  angular.module('blogControllers', []);

blogControllers.controller('BlogCtrl', ['$scope',
function BlogCtrl($scope) {

  $scope.blogArticle = 
    "This is a blog post about AngularJS. 
    We will cover how to build a blog and how to add 
    comments to the blog post.";
}]);

Next is the code for the app.js file that starts the booting process for the blog application. This is where we define the route for the main page of the blog. As you can see, we define ngRoute and blogControllers as dependencies of the application at startup time, using inline array annotations. The two dependencies are injected into the application using DI and are available throughout the application when we need them. Any controllers attached to the blogControllers module are accessible to the blogApp module (the AngularJS application):

/* chapter5/app.js */

'use strict';
/* App Module */

var blogApp = angular.module('blogApp', [
  'ngRoute',
  'blogControllers'
]);

blogApp.config(['$routeProvider', '$locationProvider',
  function($routeProvider, $locationProvider) {
    $routeProvider.
      when('/', {
        templateUrl: 'partials/main.html',
        controller: 'BlogCtrl'
  });

  $locationProvider.html5Mode(false).hashPrefix('!');
  }]);

The routes are defined in the application configuration block. For now, we will only define the main page of the blog. We define BlogCtrl as the controller and 'partials/main.html' as the template used for the main route. We will add more routes as we need them.

Twitter Bootstrap

You should have already added bootstrap.min.js to the project. If you run into JavaScript errors related to Twitter Bootstrap, you can easily replace the bootstrap.min.js file with the nonminified bootstrap.js file distributed by Twitter. Using the nonminified version of the file allows the developer to place breakpoints in the Bootstrap JavaScript file and debug any related issues. We will only cover the basics of Twitter Bootstrap here. For more documentation and tutorials on Bootstrap, see the project site.

First, we need to add three more folders and some additional Twitter Bootstrap files to the project. We will add all the Bootstrap files here, although much of Bootstrap is not actually used in this project. Do the following:

  1. Add a subfolder named css under the Site Root folder.
  2. Add a subfolder named fonts under the Site Root folder.
  3. Add a subfolder named lib-css under the Site Root folder.
  4. Copy the bootstrap-theme.min.css and bootstrap.min.css files into the lib-css folder.
  5. Copy the following files to the fonts folder:
    1. glyphicons-halflings-regular.eot
    2. glyphicons-halflings-regular.svg
    3. glyphicons-halflings-regular.ttf
    4. glyphicons-halflings-regular.woff
  6. Add the two lines of code shown next to the index.html file. These two lines are all that we need to make use of Twitter Bootstrap:
<!-- chapter5/index.html excerpt -->

<link rel="stylesheet" href="lib-css/bootstrap.min.css" media="screen"/> 

<script src="js/libs/bootstrap.min.js"></script>

Here is the completed index.html file:

<!-- chapter5/index.html complete file -->

<!DOCTYPE html>
<html lang="en" ng-app="blogApp">

<head>
<title>Blog</title>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<link rel="stylesheet" href="lib-css/bootstrap.min.css" media="screen"/>
<script src="js/libs/bootstrap.min.js"></script>
<script src="js/libs/jquery-1.10.2.min.js"></script>
<script src="js/libs/angular.min.js"></script>
<script src="js/libs/angular-route.min.js"></script>
<script src="js/libs/angular-resource.min.js"></script>
<script src="js/libs/angular-cookies.min.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>

</head>

<body>
<div ng-view></div>
</body>

</html> 

Figure 5-2 shows the project file structure. Make sure your project is set up as shown. The added CSS files and fonts will give us access to many time-saving features of Twitter Bootstrap. We will now add a Bootstrap menu to our project.

Alt Text
Figure 5-2. The completed file structure for the blog project

Adding a Bootstrap Menu

The following are the contents of the menu.html file. Most of the code shown is clearly explained on the Bootstrap project site. The styles added to the menu here are defined in the bootstrap.min.css file added in the previous section. If you have questions on Bootstrap menus, please refer to the Bootstrap project documentation for a fuller explanation. Your menu.html file should look like this:

<!-- chapter5/menu.html -->

<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">

<!-- Brand and toggle get grouped for better mobile display -->
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" 
  data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" style="{{brandColor}}" href="#!/">Angular Blog</a>
</div>

<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse">

<ul class="nav navbar-nav">
<li class="{{aboutActiveClass}}"><a href="#!about">About</a></li>
<li class="">
<a href="https://github.com/KenWilliamson">Download Project Code</a></li>
</ul>

</div><!-- /.navbar-collapse -->

</div>
</nav>

Here’s how to add the menu.html file inside the main.html file:

<!-- chapter5/main.html -->

<div ng-include src="'partials/menu.html'"></div>

{{blogArticle}} 

The first line shows the needed addition to main.html. As you see, we use the ng-include directive to include the menu template inside the main template. This approach allows us to keep the menu completely separate from the other templates. Using this approach makes the code base easy to maintain and understand. We will now focus on using other Bootstrap styles to enhance our blog.

Adding Mock Blog Data

We will modify the BlogCtrl controller and set a list of blog posts as a scope property named blogList. The modified controllers.js code is shown here. The JSON list represents the data that will eventually be retrieved from a REST service. For now, however, we will just hardcode the JSON into the controller as mock data. There are more advanced ways to add mock data to an AngularJS application, but that is beyond the scope of this book. Let’s take a look at the controllers file:

/* chapter5/controllers.js */

'use strict';
/* Controllers */
var blogControllers = 
  angular.module('blogControllers', []);

blogControllers.controller('BlogCtrl', ['$scope',
  function BlogCtrl($scope) {
    $scope.blogList = [
      {
        "_id": 1,
        "date": 1400623623107,
        "introText": "This is a blog post about AngularJS. 
          We will cover how to build",
        "blogText": "This is a blog post about AngularJS. 
          We will cover how to build a blog and how to add 
          comments to the blog post."
     },
     {
       "_id": 2,
       "date": 1400267723107,
       "introText": "In this blog post we will learn how to 
         build applications based on REST",
       "blogText": "In this blog post we will learn how to 
         build applications based on REST web services that 
         contain most of the business logic needed for the 
         application."
     }
  ];
}]);

As you can see, there is no presentation logic in this code, and no data formatting is done in the controller. The date, for instance, is sent to the view as a long value that is a standard representation of a date in most programming languages. Trying to format the date in the controller would be an incorrect design that shouldn’t be used. AngularJS has many features that make formatting and presenting data easy; we’ll look at some of these next.

Using CSS3 to Style the Page

Now we will add some CSS3 to style our pages. Do the following:

  1. Right-click the project node and create a new CSS file named style.css.
  2. Place the following code into the new CSS file:
/* chapter5/styles.css */

body{
  font-family: arial;
  font-size: 12pt;
  color: #2a6496;
}

.post-wrapper{
  float: left;
  width: 100%;
  margin: 5% 0 0 0;
  padding: 0 0 0 0;
}

.blog-post-label{
  float: left;
  width: 100%;
  margin: 10% 0 0 0;
  padding: 0 0 0 0;
  text-align: center;
  font-weight: bold;
  font-size: 16pt;
}

.blog-post-outer{
  float: left;
  width: 60%;
  margin: 2% 0 2% 20%;
  padding: 1%;
  background: #e0e0e0;
  border-radius:6px;
  -moz-border-radius:6px; /* Firefox 3.6 and earlier */
  border: darkgreen solid 1px;
}

.blog-intro-text{
  float: left;
  width: 100%;
  margin: 0 0 0 0;
  padding: 0 0 0 0;
  text-align: center;
}

.blog-read-more{
  float: left;
  width: 100%;
  margin: 2% 0 0 0;
  padding: 0 0 0 0;
  text-align: center;
}

Now modify the index.html file, adding the line shown here to load the newly created CSS file:

<!-- chapter5/index.html excerpt -->

<link rel="stylesheet" href="css/styles.css" media="screen"/>

The complete index.html file is shown here. Make sure your version of the file matches this one:

<!-- chapter5/index.html complete file -->

<!DOCTYPE html>
<html lang="en" ng-app="blogApp">

<head>
<title>Blog</title>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="lib-css/bootstrap.min.css" media="screen"/>

<script src="js/libs/jquery-1.10.2.min.js"></script>
<script src="js/libs/bootstrap.min.js"></script>
<script src="js/libs/angular.min.js"></script>
<script src="js/libs/angular-route.min.js"></script>
<script src="js/libs/angular-resource.min.js"></script>
<script src="js/libs/angular-cookies.min.js"></script>

<link rel="stylesheet" href="css/styles.css" media="screen"/>

<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
</head>

<body>
<div ng-view></div>
</body>

</html> 

Adding Styles and Presentation Logic

You must modify the main.html template to make use of the new styles and to add proper presentation logic for displaying blog posts and formatting data. Modify your main.html to match the code shown here. The second line, <div id="container" class="container">, sets up a Bootstrap container and is standard practice with Twitter Bootstrap:

<!-- chapter5/main.html -->

<div ng-include src="'partials/menu.html'"></div>

<div id="container" class="container">

<div class="blog-post-label">Blog Posts</div>
<div class="post-wrapper">
<div ng-repeat="blogPost in blogList">
<div class="blog-post-outer">

<div class="blog-intro-text">
Posted: {{blogPost.date | date:'MM/dd/yyyy @ h:mma'}}
</div>

<div class="blog-intro-text">
{{blogPost.introText}}
</div>

<div class="blog-read-more">
<a href="#!blogPost/{{blogPost._id}}">Read More</a>
</div>

</div>
</div>
</div>
</div> 

The Bootstrap container handles much of the page styling for various screen sizes to make the page responsive for any screen size on any device. Inside the container we use the CSS that was added in the styles.css file. We won’t focus much on the custom CSS, because it is not specific to AngularJS and is covered in many other books on Cascading Style Sheets.

We will, however, take a look at the AngularJS directives that allow us to build the presentation logic in the view and handle formatting. The line  <div ng-repeat="blogPost in blogList"> is very important to understanding AngularJS views. The directive ng-repeat works like a for loop, iterating over the list of blog posts in the scope property blogList.

Each iteration through the list gives access to each item in the list through the variable blogPost. We use the line {{blogPost.introText}} to display the intro text (the value of the introText property of the blogPost variable).

Another line that is very important is the HTML template binding {{blogPost.date | date:'MM/dd/yyyy @ h:mma'}}, which allows us to format the date in the view, where it should be formatted. As I stated previously, there are many features of AngularJS for formatting data, and this is just one. As you can see, the template code is simple and easy to understand.

We will now add a controller, route, and view to display the individual blog post when a user clicks on the “View More” link. If you look closely, you can see that the link passes blogPost.id as a path parameter argument to a new route, /blogPost. We will now add the needed code to view a blog post.

Viewing the Blog Post

To add the extra functionality, first append this CSS code to the end of the styles.css file:

/* chapter5/styles.css excerpt */

.blog-entry-wrapper{
  float: left;
  width: 100%;
  margin: 1% 0 0 0;
  padding: 0 0 0 0;
}

.blog-entry-outer{
  float: left;
  width: 60%;
  margin: 2% 0 2% 20%;
  padding: 1%;
  background: #e0e0e0;
  border-radius:6px;
  -moz-border-radius:6px; /* Firefox 3.6 and earlier */
  border: darkgreen solid 1px;
}

.blog-comment-wrapper{
  float: left;
  width: 50%;an HTML5 project
  margin: 2% 0 2% 25%;
  padding: 1%;
  border-radius:6px;
  -moz-border-radius:6px; /* Firefox 3.6 and earlier */
  border: darkgreen solid 1px;
}

.blog-entry-comments{
  float: left;
  width: 96%;
  margin: 2% 0 2% 2%;
  padding: 1%;
  background: #f5e79e;
  border-radius:6px;
  -moz-border-radius:6px; /* Firefox 3.6 and earlier */
  border: darkgreen solid 1px;
}

.blog-comment-label{
  float: left;
  width: 100%;
  margin: 1% 0 0 0;
  padding: 0 0 0 0;
  text-align: center;
  font-weight: bold;
  font-size: 16pt;
}

Then add this code to the bottom of the controllers.js file:

/* chapter5/controllers.js excerpt */

blogControllers.controller('BlogViewCtrl', 
  ['$scope', '$routeParams',
    function BlogViewCtrl($scope, $routeParams) {

      var blogId = $routeParams.id;
      var blog1 = {
        "_id": 1,
        "date": 1400623623107,
        "introText": "This is a blog post about AngularJS. 
          We will cover how to build",
        "blogText": "This is a blog post about AngularJS. 
          We will cover how to build a blog and how to add 
          comments to the blog post.",
        "comments" :[
          {
            "commentText" : "Very good post. I love it."
          },
          {
            "commentText" : "When can we learn services."
          }
        ]
     };

  var blog2 = {
    "_id": 2,
    "date": 1400267723107,
    "introText": "In this blog post we will learn how to 
      build applications based on REST",
    "blogText": "In this blog post we will learn how to 
      build applications based on REST web services that 
      contain most of the business logic needed for the application.",
    "comments" :[
      {
        "commentText" : "REST is great. I want to know more."
      },
      {
        "commentText" : "Will we use Node.js for REST services?."
      }
    ]
  };

  if(blogId === '1'){
    $scope.blogEntry = blog1;
  }else if(blogId === '2'){
    $scope.blogEntry = blog2;
  }


}]);

Next, add a new template file named blogPost.html in the partials folder and replace the generated code with the code shown here:

<!-- chapter5/blogPost.html -->

<div ng-include src="'partials/menu.html'"></div>

<div id="container" class="container">

<div class="blog-post-label">Blog Entry</div>
<div class="blog-entry-wrapper">

<div class="blog-intro-text">
Posted: {{blogEntry.date| date:'MM/dd/yyyy @ h:mma'}}
</div>

<div class="blog-entry-outer">
{{blogEntry.blogText}}
</div>

<div class="blog-comment-wrapper">
<div class="blog-comment-label">Blog Comments</div>
<div class="blog-entry-comments" ng-repeat="comment in 
blogEntry.comments">
{{comment.commentText}}
</div>

</div>
</div>
</div>

And add this code to the route provider section of app.js:

/* chapter5/app.js excerpt */

.when('/blogPost/:id', { 
templateUrl: 'partials/blogPost.html', 
controller: 'BlogViewCtrl'

The complete route definition is shown here:

/* chapter5/app.js excerpt - complete route */

blogApp.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {

 $routeProvider.
   when('/', {
     templateUrl: 'partials/main.html',
     controller: 'BlogCtrl'
   }).when('/blogPost/:id', {
     templateUrl: 'partials/blogPost.html',
     controller: 'BlogViewCtrl'
  });

 $locationProvider.html5Mode(false).hashPrefix('!');
}]);

As you can see, the effort required to add a new page was minimal. If you look at the route definition, you’ll see the id passed as a path parameter argument. Look at the new controller and you can see how we handle the id parameter. Since we do not yet have REST services in place, we hardcoded the JSON for the two blog posts into the controller.

Once we retrieve the passed id from $routeParams, we use that to determine which blog entry to set as a scope property. Notice that we never actually set a scope property until we know which blog entry gets sent to the view. Notice also that blog1 and blog2 are defined as local variables. Only the variables needed in the page are set as scope properties.

Warning
You should never add properties to the scope that are not needed in the view.

Running the Blog Application

Now let’s run the project to test our work. Right-click the project node and select “Run” from the menu. If you made all the changes correctly, you should see the screen shown in Figure 5-3. If you get a different result, go back over the changes in this chapter and verify that you made all the needed modifications.

Alt Text
Figure 5-3. Successful result from running the project

If you have problems that you can’t resolve, download the project code from GitHub and run that code. Once the project is running, click the “Read More” link on the first blog post. You should then see the screen shown in Figure 5-4. Click the “Read More” link on the second blog post, and you should see a similar page.

Alt Text
Figure 5-4. Viewing the comments on the first blog post

Testing with Karma

We will use Karma now to test our view. From the root of the Chapter 5 project, create a JSON file named package.json and add the following contents. The package.json file is used as a configuration file for Node.js, as mentioned in Chapter 4:

{
    "name": "package.json",
    "devDependencies": {
        "karma": "*",
        "karma-chrome-launcher": "*",
        "karma-firefox-launcher": "*",
        "karma-jasmine": "*",
        "karma-junit-reporter": "*",
        "karma-coverage": "*"
    }
}

Open a command-line window on your system, and navigate to the root of the Chapter 5 project. You should see the package.json file when you list out the files in the folder. Now type the following command to install the Node.js dependencies defined in the package.json file. This is the same process described in Chapter 4:

npm install

Karma Configuration

Now we will create a new Karma configuration file named karma.conf.js inside the project’s test folder, as we did in Chapter 4. Do the following:

  1. Right-click the project in NetBeans.
  2. Select “New”→“Other”→“Unit Tests.”
  3. Create a new Karma configuration file inside the test folder.

Edit the new karma.conf.js file and add the code shown here:

/* chapter5/karma.conf.js */

module.exports = function (config) {
    config.set({
        basePath: '../',
        files: [
            "public_html/js/libs/angular.min.js",
            "public_html/js/libs/angular-mocks.js",
            "public_html/js/libs/angular-route.min.js",
            "public_html/js/*.js",
            "test/**/*Spec.js"
        ],
        exclude: [
        ],
        autoWatch: true,
        frameworks: [
            "jasmine"
        ],
        browsers: [
            "Chrome",
            "Firefox"
        ],
        plugins: [
            "karma-junit-reporter",
            "karma-chrome-launcher",
            "karma-firefox-launcher",
            "karma-jasmine"
        ]
    });
};

Now do the following to configure Karma as the test framework:

  1. Right-click the project.
  2. Select “Properties.”
  3. Select “JavaScript Testing” from the list of categories.
  4. Select “Karma” as the testing provider.
  5. Select the location of the karma-cli tool installed in Chapter 4.
  6. Select the location of the karma.conf.js file just created, and select “OK.”

Karma Test Specifications

Now we need to add new test specifications for the Chapter 5 project. Do the following:

  1. Create a new folder named unit under the test folder of the project.
  2. Create a new JavaScript file named controllerSpec.js under the unit folder.
  3. Enter the code shown here in the new file:
/* chapter5/controllerSpec.js */

describe('AngularJS Blog Application', function () {

    beforeEach(module('blogApp'));

    describe('BlogCtrl', function () {
        var scope, ctrl;

        beforeEach(inject(function ($rootScope, $controller) {

            scope = $rootScope.$new();
            ctrl = $controller('BlogCtrl', {$scope: scope});
        }));

        it('should create show blog entry count', function () {
            console.log("blogList:" + scope.blogList.length);
            expect(scope.blogList.length).toEqual(2);
        });
    });

    describe('BlogViewCtrl', function () {
        var scope, ctrl, $httpBackend;

        beforeEach(inject(function (_$httpBackend_, 
          $routeParams, $rootScope, $controller) {
            $httpBackend =  _$httpBackend_;
            $httpBackend.expectGET('blogPost').respond({_id: '1'});

            $routeParams.id = '1';

            scope = $rootScope.$new();
            
            ctrl = $controller('BlogViewCtrl', {$scope: scope});
        }));

        it('should show blog entry id', function () {            
            expect(scope.blogEntry._id).toEqual(1);
        });
    });
});

Karma Testing

The new test specification will unit test both controllers. Right-click the project and select “Test” from the menu. Karma will start. You should see both Chrome and Firefox browser windows open. The NetBeans test results window should open and display two passed tests for Chrome and two passed tests for Firefox.

If you get any error messages or failed tests, go back over this section and verify that you completed all the configurations and installations. You can also download the Chapter 5 code from the GitHub project site.

End-to-End Testing

Next, we need to create a Protractor configuration file for the project. Create a new JavaScript file named conf.js under the test folder of the Chapter 5 project. Enter the code shown here in the new file:

/* chapter5/conf.js Protractor configuration file */

exports.config = { 
  seleniumAddress: 'http://localhost:4444/wd/hub', 
  specs: ['e2e/blog-spec.js'] 
};

Protractor Test Specification

Now we need to create a Protractor test specification. Do the following:

  1. Create a new folder under the test folder of the project and name it e2e.
  2. Create a new JavaScript file inside the new e2e folder and name it blog-spec.js.

Then copy the code shown next into the new blog-spec.js file.

Warning

Make sure the lines browser.get("http://localhost:8383/AngularJsBlog/"); match the URL that you use on your system to call the blog application. The URL can be different for different development environments and can depend on how you named your project.

/* chapter5/blog-spec.js */

describe("Blog Application Test", function(){
    it("should test the main blog page", function(){
        
        browser.get("http://localhost:8383/AngularJsBlog/");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the blog list
        var blogList = element.all(by.repeater('blogPost in blogList'));
        
        //tests the size of the blogList
        expect(blogList.count()).toEqual(2);        
        browser.get(
       "http://localhost:8383/AngularJsBlog/#!/blogPost/1");
        expect(browser.getTitle()).toEqual("AngularJS Blog");
        
        //gets the comment list
        var commentList = element.all(
         by.repeater('comment in blogEntry.comments'));
        
        //checks the size of the commentList
        expect(commentList.count()).toEqual(2);        
    });
});

Protractor Testing

Start a new command window and enter the following command to start the test server:

webdriver-manager start

Open a new command window and navigate to the root of the Chapter 5 project. Type the command:

protractor test/conf.js

You should see a browser window open. You should then see the test script navigate through the pages of the blog application. When the Protractor script has finished, the browser window will close.

You should see results like the following in the command window when the Protractor script completes. The number of seconds that it takes the script to finish will vary depending on your particular system:

Finished in 1.377 seconds
1 test, 4 assertions, 0 failures