Chapter 3

The BookMonkey Project

After discussing the basic concepts of AngularJS, we now want to use what we know so far to build a real-world application with AngularJS. We will also base it on specific use cases to introduce other mechanisms that make AngularJS great.

Project and Process Presentation

The project that we are implementing is called BookMonkey. It is a small web application to browse a book collection, view detailed information about a book and tag a book to put it in a category. There will also be an administration area that allows an administrator to manage the database.

The implementation will be carried out in iterative steps. The order of these steps is chosen such that they will provide the highest educational value. For example, even if it would be preferable in a real project, the test-driven development approach will not be the first step. We could shed light on many aspects of a popular approach and might lose focus on the core functionality of the framework. Therefore, we will introduce the test-first approach only in some future steps.

In order to make the goals of the individual steps tangible, there is a user story to be met at each iteration.

The project backend system is not the subject of this chapter. However, we have created a rudimentary backend implementation based on Node.js that you can use.

Requirements

To run the examples in this project, you need a modern browser. We tested the examples in Chrome and recommend the latest version of this browser. However, every other browser that implements the ECMAScript language standard version 5 or later should be able to run the project easily. Google Chrome can be downloaded for free from this site.

http://www.google.com/chrome/

In addition, the example should be loaded from a local web server. For this purpose, you can use the http-server module of Node.js. This module can be downloaded from this site.

http://npmjs.org/package/http-server

Using http-server you can start an HTTP server from any directory. Before you can install the http-server module, however, you need a current version of Node.js and npm installed on your machine. You can download Node.js for free from its official website:

http://nodejs.org/download

After Node.js and npm have been successfully installed, you can install the http-server module by using npm. Use this command on the console to install it:

npm install -g http-server

After a successful installation, you can start http-server using this command on the console:

http-server

By default a lightweight HTTP server will be started on port 8080. The HTTP server will deliver all files located in the directory where the server is running.

You also need Karma to run the tests. Karma is also installed as a Node.js module. Its official website is this.

http://karma-runner.github.io

Use the following console command to install Karma.

npm install -g karma

Finally, you need to install the Karma support for the ngScenario framework. This is the test framework for E2E testing that AngularJS brings with it. Use this command on the console to install it:

npm install-g karma-ng-scenario

Setting up the Project Environment

Before you begin the actual application development, you first need to set up the project environment. To do this, create a new directory named bookmonkey. In this directory, you define a basic structure by creating two subdirectories called app and test. This way, you separate tests, which you will write later, from the actual application. In the app directory, create an HTML file named index.html. This file is delivered as the default page by most web servers when the user does not reference a specific file in the address bar. Thus, it serves as the entry point into the application. The content of this file is shown in Listing 3.1.

Listing 3.1: The initial index.html of our application

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>BookMonkey</title>
</head>
<body>
    <h1>Hello BookMonkey</h1>
</body>
</html>

Now change directory to the app directory and start the aforementioned HTTP server using this familiar command:

http-server

The command should return a successful message and tell you the port number on which the server is started.

Starting up http-server, serving ./ on port: 8080 
Hit CTRL-C to stop the server 

This statement simply states that you can stop the server by pressing CTRL+C. Of course, you need to leave it run for now.

You can now browse to this URL to retrieve your currently static HTML page:

http://localhost:8080

Figure 3.1: The output of the initial index.html page

If you see something similar to Figure 3.1 in your browser, you have successfully completed this step and can proceed with the AngularJS integration.

In the next step we want to integrate AngularJS in our project. In this project we use AngularJS version 1.2.10. The library we can be downloaded from its official website here.

Http://code.angularjs.org/1.2.10/angular.js

In the project the file is stored under the path app/lib/angular/angular.js. To do this you need to create a lib directory under the app directory. The lib directory in turn has a subdirectory called angular. While you are at it, you should also download the ngRoute module from http://code.angularjs.org/1.2.10/angular-route.js and put the file as app/lib/angular-route/angular-route.js. You will need this module to use the routing functionality of AngularJS.

Next, you need to include the framework using a <script> tag in your index.html page. You should add this tag to the end of our <body> tag. The <script> tag for including the ngRoute module (angular-route.js) should follow immediately. To quickly check if AngularJS is mounted correctly, we extend our index.html page to a simple two-way data binding. For this purpose, we have to tell AngularJS in which DOM section we want to create a AngularJS application. We know that we have to use the ngApp directive, and by annotating the <html> tag with the ng-app attribute, we define it for the entire DOM. Thus, we tell AngularJS that it should consider and evaluate the entire range as an AngularJS application. Finally, we build a simple example of a bidirectional data binding in order to make sure that our AngularJS was mounted correctly.

Listing 3.2: Our application now needs AngularJS

<!DOCTYPE html> 
<html ng-app> 
<head> 
    <meta charset="utf-8"> 
    <title>BookMonkey</title> 
</head>

<body ng-init="name='BookMonkey'"> 
    <h1>Hello {{ name }}</h1> 
    <input type="text" ng-model="name" /> 

    <!-- Scripts --> 
    <script src="lib/angular/angular.js"></script> 
    <script src="lib/angular-route/angular-route.js"></script> 
</body> 
</html>

Here we create a new text field and use the ngModel directive to bind it to the scope variable name. We also replace our heading with the expression Hello {{name}} and give the ngInit directive the initial value for scope variable name. Our index.html page should now look like that in Listing 3.2.

Figure 3.2: The output of our index.html after including AngularJS

If you reload our browser window, the page should look like that in Figure 3.2. It already supports interaction. If you change the text in our box, the text in the header will also be updated immediately. The two-way data binding works. This means you have successfully integrated AngularJS in our project.

Why use automatic update at this point? And, what exactly is happening here? Because we annotate the <html> with the ngApp directive, AngularJS will evaluate all directives and expressions within this DOM section. In practice this means the whole DOM is evaluated. By using the ngInit directive in the <body> tag, we put the scope variable name in the scope and assign to it the string “BookMonkey”. Since we have previously used no constructs, we initialize the variable in the so-called $rootScope. Each AngularJS application has such a $rootScope and it always represents the root of the resulting scope hierarchy, hence the name $rootScope.

The expression Hello {{name}} in the <h1> tag triggers AngularJS to use the scope in this area. Practically, the framework checks if the scope variable name is defined in $rootScope, and, if so, displays its value. In addition, the expression is evaluated every time the value of the scope variable name has changed.

By using the ngModel directive in the <input> tag, we establish two-way data binding on the scope variable name. Thus, any change to the scope variable name will be reflected in the <input> field. The same is also true in the opposite direction. If the input field changes, the scope variable name will also be automatically updated.

Project Start: The Book Details View

Now that we have laid the foundation for the project and successfully integrated AngularJS, we now need to take care of the BookMonkey project. To this end, we introduce more concepts and mechanisms of the framework to solve relevant problems. So let’s look at our first task, which we define in the form of a user story:

The central aspect of this story is the display of detailed information about a particular book. For this purpose, we should first make a wireframe of the view (see Figure 3.3).

Based on this wireframe we try to derive a data model that leads us directly to a JSON structure. We opt for the data model in Listing 3.3.

Figure 3.3: The wireframe of the Details view

Listing 3.3: The data model for the detailed view of a book in JSON format

{
  "title"    : "JavaScript for Enterprise Developers", 
  "subtitle" : "Professional Programming in the Browser"
      + "and on the Server", 
  "isbn"     : "978-3-89864-728-1", 
  "abstract" : "JavaScript is no longer only interesting to"  
      + " classic web programmers.", 
  "numPages" : 302, 
  "author" : "Oliver Ochs", 
  "publisher": { 
    "name" : "dpunkt.verlag", 
    "url" : "http://dpunkt.de/"
  } 
}

The Template for the Details View with Expressions

We continue with the creation of a template for this view. For this purpose, we create a new directory called templates for our templates in the app directory. In this new directory we create an HTML file named book_details.html. This file will contain the definition of the template.

Listing 3.4: The template for the book Details view

<h2>{{ book.title }}</h2> 
<h3>{{ book.subtitle }}</h3> 
<p> 
  <ul> 
    <li>ISBN: {{ book.isbn }}</li> 
    <li>Number of Pages: {{ book.numPages }}</li> 
    <li>Author: {{ book.author }}</li> 
    <li>
      Publisher: 
      <a ng-href="{{ book.publisher.url }}" 
        target="_blank"> 
        {{ book.publisher.name }} 
      </a>
    </li> 
  </ul> 
</p> 
<hr> 
<p> 
  {{ book.abstract }} 
</p>

Listing 3.4 shows the HTML code of the template for the book Details view. We use expressions {{book.title}}, {{book.subtitle}}, etc to display relevant accounting information. In order for these expressions to be evaluated successfully, the scope variable book must be in the scope that is valid for this view, and book must point to an object that contains the corresponding properties (title, subtitle, etc). We will define the scope for this view using a controller. However, first let's look at yet another feature of the template in Listing 3.4.

Templates with ngBind and ngBindTemplate

Consider the HTML template in Listing 3.5.

Listing 3.5: Displaying book information with ngBind

<h2 ng-bind="book.title"></h2> 
<h3 ng-bind="book.subtitle"></h3> 
<p> 
  <ul> 
    <li ng-bind-template="ISBN: {{ book.isbn }}"></li> 
    <li ng-bind-template="Number of Pages: {{ book.numPages }}"></li>
    <li ng-bind-template="Author: {{ book.author }}"></li> 
    <li> 
      Publisher:
      <a ng-bind="book.publisher.name" 
        ng-href="{{ book.publisher.url }}" 
        target="_blank"> 
      </a> 
    </li> 
  </ul> 
</p> 
<hr> 
<p ng-bind="book.abstract"> 
</p>

In Listing 3.5 we have largely replaced the expressions with special ng-bind attributes. The ngBind directive that defines the ng-bind attribute is also part of the standard repertoire of the framework. Technically, the directive binds the value of a scope variable that is specified as an attribute value to an annotated HTML element. Finally, using this directive produces the same output as with the equivalent expressions. Compared to an expression, however, the ngBind directive has two decisive advantages:

  1. 1. Before AngularJS finishes evaluating an expression, the expression can be seen as it is on the view. This does not happen with ngBind.
  2. 2. When generating output from expressions, AngularJS always creates a new DOM element to replace the old DOM element that contains the old value, if the value of the corresponding variable scope has changed. If you instead use the ngBind directive to generate output, the framework creates no new DOM element and instead updates the text content of the existing DOM element with the new value of the scope variable. Compared to an expression, ngBind has a performance advantage and usually consumes lower memory within our application.

In addition, the template in Listing 3.5 also uses a directive that is related to ngBind, the ngBindTemplate directive. As the name of the directive implies, you can use it to define a template for the text content of an HTML element. The ngBindTemplate directive is useful when we want to generate output that combines a static string with the value of a variable scope. In addition, ngBindTemplate can also produce output that mixes the values of multiple scope variables, because we can specify multiple expressions with this directive. The ngBindTemplate directive also has the same advantages as ngBind over simple expressions.

Defining the Application Module

After optimizing the template, we now want to implement the controller that provides the scope for this view and resides in the scope of the book variable with a corresponding Book object. In addition, we want to create our first route. To do this we need to extend our project structure and the index.html file. The new version of our index.html file is shown in Listing 3.6.

Listing 3.6: An advanced version of index.html

<!DOCTYPE html> 
<html ng-app="bmApp"> 
<head> 
  <meta charset="utf-8"> 
  <title>BookMonkey</title> 
</head> 
<body> 
  <header> 
    <h1>BookMonkey</h1> 
  </header> 

  <div ng-view> 
  </div> 

  <!-- Scripts --> 
  <script src="lib/angular/angular.js"></script> 
  <script src="lib/angular-route/angular-route.js">
  </script> 
  <script src="scripts/app.js"></script> 
  <script 
    src="scripts/controllers/book_details.js">
  </script> 
</body> 
</html>

Listing 3.6 shows a module, specified using the ngApp directive, which AngularJS should load automatically when the DOM has finished loading. Once the browser has loaded the DOM, the bmApp module should be started. At this point we can already anticipate that our BookMonkey application will only have one module. Within this module we will therefore define and implement all application components.

The First Route

Looking at the <script> tags in Listing 3.6, we realize we need two more JavaScript files: app.js and book_details.js. The app.js file will contain the definition of our bmApp module and route definition. In the book_details.js file we will implement the BookDetailsCtrl controller that provides the data and functionality for the book Details view. To do this we first create in a directory named scripts under the app directory. In this new directory we will store the entire JavaScript source code of the BookMonkey application. Next, we create a file called app.js in the scripts directory. Below the scripts directory we also create a directory called controllers, in which we create the book_details.js file. At the same time, we will also implement another controller in our application and store it in the controllers directory. In addition to the controllers directory, later in the next steps we will also create more directories in the scripts directory for directives, services and filters.

Listing 3.7: The definition of bmApp module and the first route in app.js

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

bmApp.config(function ($routeProvider) { 
    $routeProvider.when('/books/:isbn', { 
        templateUrl: 'templates/book_details.html', 
        controller: 'BookDetailsCtrl'
    }); 
});

Listing 3.7 shows the source code for app.js. In the first line we define our bmApp module with the module() function from AngularJS. It is important that the module name is passed as the first parameter. The module name corresponds to the name used for the ngApp directive in the index.html file. The second parameter is an array, which other AngularJS modules for our bmApp module will depend on. Because we want to use routes in our BookMonkey application, we need to insert the ngRoute module at this point. This module is available, because we have included the angular-route.js file in our index.html file (see Listing 3.6).

Furthermore, we use $routeProviders to configure our first route in a config() block. Recall that we may use dependency injection to inject a service provider to configure the config() block in the configuration phase of our application. This means that we can use $routerProviders to configure the routes of the $route service. As mentioned in the previous chapter, routes are URL mappings on a template. Optionally, as shown in our example, we can also specify a controller. The $route service, which at runtime evaluates which route is active, works closely with the ngView directive, with which we have annotated the <div> container in the index.html file (See Listing 3.6). The configured templates will be loaded depending on the current URL in the innerHTML attribute of this <div> element. In addition, if a controller has been configured for a route, a new scope will be created for the template and the controller’s constructor function will be called. This relationship is illustrated in Figure 3.4.

Figure 3.4: The relationship between the ngView directive and $routeProvider

Looking at the /books/:isbn route, which is mapped to the detail view of a book, there is still a small feature. You can specify a path parameter with a colon. In this route, isbn is a path parameter. This means that this part of the URL can be a variable and will still be mapped to the configured template and controller. In a later stage of development of the book details, we can use the isbn path parameter to access the appropriate book. Within the BookDetailsCtrl controller, access is made possible by via the so-called $routeParams service.

A Separate Scope with the BookDetailsCtrl Controller

In the rudimentary implementation of BookDetailsCtrl in Listing 3.8, we are not interested in the ISBN yet. Rather, we want to assign a Book object to the scope variable book so that we can produce the first working version of the Details view. Recall that a controller for a DOM element defines its own scope. This is also true for the BookDetailsCtrl controller. In the DOM section of the template (book_details.html) AngularJS creates a new scope every time the route is invoked. In this scope, we can access the inner part of the constructor function of the controller. In the scope we finally set the book variable with a Book object containing all the properties specified in the template with the ngBind or ngBindTemplate directive.

Listing 3.8: The implementation of BookDetailsCtrl

bmApp.controller('BookDetailsCtrl', function($scope) 
{ 
  $scope.book = { 
    title : 'JavaScript for Enterprise Developers', 
    subtitle : 
      'Professional Programming in the Browser'  
          + ' and on the Server', 
    isbn : '978-3-89864-728-1', 
    abstract : 'JavaScript is no longer only'
        + ' interesting to classic web programmers', 
    numPages : 302, 
    author : 'Oliver Ochs', 
    publisher: {
        name : 'dpunkt.verlag', 
        url : 'http://dpunkt.de/' 
    }
  }; 
});

Now if you type a URL that matches the defined route in your browser, you should see the first version of the Details view like that in Figure 3.5. A possible URL is:

http://localhost:8080/#/books/123

Of course, you can also pass a value other than 123 as the path parameter. Because we have not yet handled the isbn path parameters when creating the Details view, the same Details view is always rendered.

First, the Test

As mentioned earlier, it is very important to embrace the test-driven development approach. From this perspective we have already made a mistake, because we did not write the test before the actual implementation. We therefore want to write a first simple test at this point to show that using the ngBind or ngBindTemplate directive in the template will produce the same result as the expression.

Figure 3.5: The Book Details view

We have a directory named test in our BookMonkey main directory for the tests we will write. Now in the test directory we will create two subdirectories named unit and e2e. In the unit directory we will store our unit tests, while in the e2e directory we will render E2E testing.

In summary, you use unit tests to test small application units isolated from the rest of the application. Examples of such units are controllers, services, directives and filters. In unit testing you run part of their logic and then check if the results of this execution are as expected. To write a genuine isolated test, the dependencies of the application units are usually mocked. This means you replace the real implementation of a dependency with a mock object. A mock object is an object that has the same public API provided for the tested application unit. Instead of an actual implementation, a mock object provides an implementation that returns some hardcoded values. Based on these hard-coded values, you can determine how a dependency should behave in the test case. AngularJS brings with it its own mock implementations, to save you a lot of work when conducting testing.

In E2E testing, however, you should think of your application as a black box. You have no access to the internal state of your JavaScript code and can thus formulate no expectations on the basis of this condition. Rather, you can only formulate expectations which relate to the state of the DOM. Using a domain specific language (DSL) you can control how a potential user should behave. This way, you can put the application in a particular state and then check the DOM to find out if the application is behaving correctly.

At this point we want to examine our template for the Book Details view by using an E2E test. For this, however, we have to expand our template again to make it able to easily reference the corresponding DOM node whose content we want to check in the test.

Listing 3.9: Extending our template to include CSS classes

<h2 ng-bind="book.title" class="bm-book-title"></h2> 
<h3 ng-bind="book.subtitle" 
    class="bm-book-subtitle"></h3> 
<p> 
  <ul> 
    <li ng-bind-template="ISBN: {{ book.isbn }}" 
      class="bm-book-isbn"></li> 
    <li ng-bind-template=
"Number of Pages:{{ book.numPages }}" 
      class="bm-book-num-pages"></li> 
    <li ng-bind-template="Author: {{ book.author }}" 
      class="bm-book-author"></li> 
    <li> 
      Publisher: <a ng-bind="book.publisher.name" 
        ng-href="{{ book.publisher.url }}" 
        target="_blank" 
        class="bm-book-publisher-name"> 
      </a>
    </li> 
  </ul> 
</p> 
<hr> 
<p ng-bind="book.abstract" class="bm-book-abstract"> 
</p>

Listing 3.9 shows the markup of our template after the extension. As you can see, there are HTML elements that contain the relevant book information, supplemented by CSS classes (such as bm-book-title). This allows us to formulate short CSS selectors to reference these HTML elements and check its contents in the E2E test.

Before writing the actual E2E test, we need to create a subdirectory named templates in the e2e directory. We also need to create a book_details.spec.js file in templates and define the E2E test for book details in the file.

Listing 3.10: E2E test for the template

describe("E2E: book details view", function() { 

  beforeEach(function() { 
      browser().navigateTo('/'), 
  }); 

  it('should show the correct book details', function() { 
    browser().navigateTo('#/books/978-3-89864-728-1'),

    expect(element('.bm-book-title').html()).toBe( 
      'JavaScript for Enterprise Developers'
    );
    expect(element('.bm-book-subtitle’).html())
      .toBe( 'Professional Programming in the Browser'
          + ' and on the Server' 
    );
    expect(element('.bm-book-isbn').html())
      .toBe( 'ISBN: 978-3-89864-728-1'
    );
    expect(element('.bm-book-num-pages').html())
      .toBe( 'Number of Pages: 302'
    ); 
    expect(element('.bm-book-author’).html())
      .toBe( 'Author: Oliver Ochs'
    ); 
    expect(element('.bm-book-publisher-name')
      .html()).toBe( 'dpunkt.verlag'
    );
    expect(element('.bm-book-publisher-name')
      .attr('href')).toBe( 'http://dpunkt.de/' 
    );
    expect(element('.bm-book-abstract').html())
      .toBe( 'JavaScript is no longer only interesting '
          + ' to classic web programmers.' 
    ); 
  }); 
});

Listing 3.10 shows the E2E test for the book details (See book_details.spec.js), which checks whether the data from the scope is returned correctly. If you are familiar with Jasmine testing framework (http://pivotal.github.io/jasmine) , you will probably immediately see the parallels. In fact, AngularJS ngScenario, which is used for E2E testing, was designed based on Jasmine.

Basically E2E tests are constructed the same way as unit tests. You start out with a describe() function that the test suite defines. The first parameter to the function is a name for the test suite. The second parameter contains an anonymous function within which you define the test cases. A single test case defined with the it() function expects as its first parameter a string that expresses the expected behavior in natural language. The second parameter is an anonymous function in which the test case is defined. It is characteristic of such a test case that it first creates a condition, then it executes the tested functionality and concludes with one or more expect() function that formulates the expected post-conditions. If several test cases require the same condition or you want to perform certain logic before each test case, then you can use the beforeEach() function. This function expects a single parameter, a function that will be executed before each test case. In our E2E Test Suite in Listing 3.10, we use the beforeEach() function to navigate to the home page of the application before each test case. The specific call looks like this.

browser().navigateTo('/')

At the current stage of development, that is our only condition that could potentially be relevant for all test cases in the test suite. Because we only define one test case, we could encode this precondition directly in the test case. However, we may add more test cases that require this condition in the future. Looking ahead, we have therefore put this in the beforeEach() function.

In our single test case, we navigate to the same URL, which we have opened manually in the browser. However, this time we add a path parameter to a real ISBN to prepare for this test case for future development. Finally, we will use it to display the details of the corresponding book.

browser().navigateTo('#/books/978-3-89864-728-1'),

Internally, our application should now be loaded according to the route configuration of the BookDetailsCtrl controller and the template for the Details view (templates/book_details.html). Then, the book information should be available in the DOM. Subsequently, we can start by using the expect() function to formulate our post-conditions. Since we have provided the appropriate HTML elements with meaningful CSS classes in our template, we can now use the element() function in conjunction with the appropriate CSS selector (e.g. bm-book-title) to reference the necessary DOM elements. What follows is calling a matcher. In ngScenario, a matcher has the same semantics as in Jasmine. There are basic matchers like the toBe() matcher, which only compares the types of two objects. More complex matchers include the toContain() matcher, which is provided by ngScenario. This matcher checks if a string is part of another string. You can also use toContain() to ensure that a certain element is contained in an array. A complete list of matchers and the descriptions of the complete API of ngScenario can be found at http://docs.angularjs.org/guide/dev_guide.e2e-testing.

We have now defined our first test suite. To execute the E2E testing suite, we now need a runtime environment. In the AngularJS world, Karma has established itself as the test execution environment, because the project virtually started when the AngularJS developers themselves were looking for a suitable tool for AngularJS. You have installed Karma at the beginning of this chapter and can use it from now on as the runtime environment for unit and E2E testing.

To run the test suite in Listing 3.10, you need a configuration file for Karma. At this point, don’t worry about what configuration options Karma provides. This aspect will be discussed in Chapter 5, “Project Management and Automation.” If you have created a project structure exactly as described, you can easily use the configuration file in the accompanying zip file. To this end, copy the karma-e2e.conf.js file in the bookmonkey directory. And, while you're at it, you can also copy the karma.conf.js file in the same directory. The karma.conf.js file is the configuration file you need to run the unit tests, while karma-e2e.conf.js is the configuration for running the E2E testing.

Now, to perform the E2E test suite for a book’s details, change directory to the root directory of the BookMonkey application (bookmonkey) and start Karma with the following command:

karma start karma-e2e.conf.js

If you have not made a mistake, Karma should give you feedback on the successful execution of a test case.

Chrome 30.0.1599: Executed 1 of 1 SUCCESS (0.908 secs / 0.699 secs)

The aim of this E2E testing is to show that the ngBind or ngBindTemplate directive can return the same output as the template that uses simple expressions.

To verify this claim, you can create another Book Details view template with expressions. However, do not forget to decorate the HTML elements with the corresponding CSS classes. Otherwise your CSS selectors from the E2E test will no longer work. The new template can be seen in Listing 3.11.

Listing 3.11: A template that uses expressions for the tests

<h2 class="bm-book-title">{{ book.title }}</h2> 
<h3 class="bm-book-subtitle">{{ book.subtitle }}</h3> 
<p> 
<ul> 
    <li class="bm-book-isbn">ISBN: {{ book.isbn }}</li> 
    <li class="bm-book-num-pages">
Number of Pages: {{ book.numPages }}</li>
    <li class="bm-book-author">Author: {{ book.author }}</li> 
    <li> 
        Publisher: 
        <a ng-href="{{ book.publisher.url }}" 
            target="_blank" class="bm-book-publisher-name"> 
            {{ book.publisher.name }}</a> 
    </li> 
</ul> 
</p> 
<hr> 
<p class="bm-book-abstract">{{ book.abstract }}</p>

If you run the E2E test again, you will see the same output with simple expressions.

Summary

  • You use expressions to return values for a template.
  • You can use expressions to output the current values of variables from the scope, which are valid for the template in its current state.
  • Expressions are subject to two-way data binding. This means they will be re-evaluated when the value of the scope variable changes.
  • Whenever possible, you should choose the ngBind or ngBindTemplate directive over expressions for performance reasons.
  • You can use the ngHref directive to create a hyperlink that contains an expression. AngularJS then ensures that the link is active only when the template is processed and the corresponding expression evaluated.
  • With $routeProvider you can configure application routes in a config() block.
  • The ngView directive is used to annotate an HTML element to tell the framework which route template should be loaded.
  • You use E2E tests to script simulated user behavior for your application and to formulate certain expectations based on the DOM.
  • Angular comes with its own test framework called ngScenario.

The List View

After we have created a basic Details view successfully, we now want to take care of the List view. This view will show a list of books that the user can select to view in the Details view. You can use the List view for multiple purposes. For instance, you can show the search results (See Figure 3.6). As another example, the List view can also be used in the Administration area to facilitate the admin management.

Figure 3.6: The proposed List view

The central aspect of this section deals with the ngRepeat directive. You use this directive to render a list-like structure like an array in a template. The ngRepeat directive provides a series of functions to filter or arrange the output.

We also formulate a user store for the List view.

First, the Test

We mentioned that we wanted to continue with the test-first approach. In this section, we first examine the tests for the List view by creating the book_list.spec.js file in the test/e2e/templates/ directory and defining an E2E test suite in the file.

Listing 3.12: Preparing the E2E test to verify the List view

describe("E2E: book list view", function () { 

    // Define the array of books in the expected order. 
    // Sorted by title. 
    var expectedBooks = [ 
        {
            title: 'CoffeeScript', 
            isbn: '978-3-86490-050-1',
            author: 'Andreas Schubert'
        },
        {
            title: 'JavaScript for Enterprise Developers',
            isbn: '978-3-89864-728-1',
            author: 'Oliver Ochs'
        },
        {
            title: 'Node.js & Co.',
            isbn: '978-3-89864-829-5',
            author: 'Golo Roden'
        }
    ];

    // Derive an array that only contains titles 
    // for easier expectation checks. 
    var orderedTitles = expectedBooks.map(function(book) {
        return book.title;
    });

    [...]

});

Listing 3.12 shows the basic structure of the test suite. Before we write the actual test cases, we define some auxiliary constructs to simplify the implementation of the test cases. The relevant auxiliary construct at this point is the expectedBooks book array. In this array, we define what we expect to see as a book entry in the List view, namely an object with title, isbn and author properties. In particular, in this definition it is important that the Book objects are in the expected order, so we can easily check in the test case if the List view has the entries sorted correctly. Here we have chosen to define the lexical order based on the book title.

In addition, we use the map() function to derive from the expectedBooks array a second array named orderedTitles, in which we only store the book titles in the same order. The orderedTitles array is an auxiliary construct for the subsequent test cases.

Listing 3.13: The beforeEach() block of the E2E test suite to verify the List view

describe("E2E: book list view", function () {

    [...]

    beforeEach(function () { 
        browser().navigateTo('#/books'), 
        browser().reload(); 
    });

    [...]

});

Listing 3.13 shows the continuation of our test suite in Listing 3.12. Just like the E2E test for the book details, here we use a beforeEach() block to produce the precondition for our tests. In this case we are telling Karma to navigate to the /books route and reload the document before each test case. The reloading is necessary because we will change the view state in some future test cases. Thus, we make sure that the view is in its original state before the execution of each test case. Simply navigating to the /books route is not enough here, because the initial state is not usually produced when we navigate between the same URLs.

Listing 3.14: The test case for verifying the correct number of books

describe("E2E: book list view", function () { 

    [...] 

    var selector = 'table.bm-book-list tr';

    it('should show the correct number of books', function () { 
        expect(repeater(selector).count()).toEqual( 
                expectedBooks.length); 
        });

    [...]

});

We continue in Listing 3.14. Since we need the CSS selector in several test cases, we store it in a variable called selector.

We now use the it() function and a repeater to write our first test case to test if the List view has as many items as the expectedBooks array has the Book objects. The repeater is a construct that ngScenario provides to conveniently retrieve information from the tabular DOM-like structures, most of which were generated using the ngRepeat directive. Examples of such tabular DOM structures include an HTML table. It is characteristic of such a structure that repeats a specific DOM pattern several times. An HTML table is therefore a perfect example of such a structure, because each row (<tr>) usually has the same number of columns (<td>) and thus has a consistent structure.

Based on the CSS selector, which we passed to the repeater as a parameter, we have implicitly determined that the List view should be based on an HTML table. This way, we can use the repeater to determine how many entries are included in our list view. The relevant function for this on the repeater is count(). Thus, we can now formulate our expectation for this test.

It continues with the next test, which checks whether the entries in the List view are sorted correctly on the book title in descending order.

Listing 3:15. A test case for verifying the correct order

describe("E2E: book list view", function () { 

    [...] 

    it('should show the books in the proper order', function() { 
        // Are they in the expected order 
        // (ascending sorted by title)? 
        expect(repeater(selector).column('book.title')).toEqual( 
                orderedTitles); 
    }); 

    [...] 

});

The corresponding test case is shown in Listing 3.15. Again, we use a repeater to read the information from the DOM. However, this time we use the column() function of the repeater, in which we can see again that a repeater is practically the supplementary test construct for the ngRepeat directive. The column() function takes as a parameter a data binding expression, which is exactly the fragment which we would use in conjunction with the ngBind directive to produce template output. At this point we determine that this expression should read book.title. Afterward, we will have to work with the following expression in the implementation to generate the output:

ng-bind="book.title"

Using the column() function we can easily read the values of the table columns that contain book titles. The function returns an array containing the corresponding column values of all rows. Therefore, we can use the toEqual() matcher to compare if the returned array corresponds to our orderedTitles array. If this expectation is fulfilled, then it means that the list entries match the order that we have manually specified in the expectedBooks array because the orderedTitles array was derived from the expectedBooks array.

What we are still missing before we proceed to the actual implementation of the List view, is the last test case that verifies that the author and ISBN are returned correctly after the book title. The test case can be found in Listing 3.16.

Listing 3.16: Test case for verifying the correct list contents

describe("E2E: book list view", function () {

    [...] 

    it('should show the correct book information', function() { 
        // Do the other book details (isbn, author) match? 
        for (var i = 0, n = expectedBooks.length; i < n; i++) { 
            expect(repeater(selector).row(i)) 
                .toEqual(
                    [
                        expectedBooks[i].title, 
                        expectedBooks[i].author, 
                        expectedBooks[i].isbn
                    ] 
                );
        } 
    });
});

The special feature of this test case is that we use a new aspect of the repeater, the row() function. The name says it all. This function takes a table row index as parameter and returns an array containing column values in that row. Therefore, we can easily express the expectation using a for loop that the book title, author and ISBN in a line must always be equivalent to the corresponding definition in the expectedBooks array. We have thus defined the last expectation on the list view. Therefore, we can now create an implementation as expected. Before we do that, we should run the E2E tests again and make sure that the tests from the test suite we just created fail because there is still no implementation.

karma start karma-e2e.conf.js

The corresponding negative feedback should look like this.

Chrome 30.0.1599: Executed 4 of 4 (3 FAILED) 
                                        (1.217 secs / 0.971 secs)

Our E2E test for the Details view is still “green,” while the three tests we just defined failed. In the output we can also find out which tests failed.

The Infrastructure for the List View

Having defined the E2E tests for the List view, we now need to first create the infrastructure before we get started with the actual implementation. This means we have to create the appropriate files and configure an additional route in our app.js file. We will start with the route. For this we need to expand the app.js file to that in Listing 3.17.

Listing 3.17: A new route for the List view

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

bmApp.config(function ($routeProvider) { 
    $routeProvider.when('/books/:isbn', { 
        templateUrl: 'templates/book_details.html', 
        controller: 'BookDetailsCtrl'
    }) 
    .when('/books', { 
        templateUrl: 'templates/book_list.html',
        controller: 'BookListCtrl' 
    });
});

We use the when function to configure another route so that all calls to /books are directed to the List view, which of course has its own template (book_list.html) and controller (BookListCtrl). Therefore, we need to create these two files and include them in the index.html file using <script> tags. We will create the book_list.html file in the app/templates/ directory and the BookListCtrl controller in a separate file named book_list.js in the app/scripts/controllers/ directory.

The BookListCtrl Controller

The BookListCtrl controller implementation is similar to the implementation of the BookDetailsCtrl controller (See Listing 3.18).

Listing 3.18: The BookListCtrl controller

bmApp.controller('BookListCtrl', function ($scope) { 
    $scope.books = [ 
        {
            title : 'JavaScript for Enterprise Developers',
            isbn : '978-3-89864-728-1',
            author : 'Oliver Ochs',
            [...]
        },
        {
             title : 'Node.js & Co.',
             isbn : '978-3-89864-829-5',
             author : 'Golo Roden',
             [...]
        },
        {
            title : 'CoffeeScript',
            isbn : '978-3-86490-050-1',
            author : 'Andreas Schubert',
            [...]
        }
    ];
});

We populate the books scope variable with an array of Book objects. Before we upload this information from the server in the next step, for now we content ourselves with a static definition. We should also note that for the sake of clarity, we have omitted irrelevant book information in Listing 3.18.

The ngRepeat Directive: Displaying An Array in the Template

To display output from a list-like structure like an array in a template, we have to use the ngRepeat directive. Besides displaying the output, this AngularJS directive can also filter, transform and assign a list. You will learn all these aspects in the sections to come.

For this purpose, we first implement the simplest version of our template, to have at least three books returned by the array. This version of the template is shown in Listing 3.19.

Listing 3.19: The template for the List view, employing the ngRepeat directive

<table class="bm-book-list"> 
    <tbody> 
        <tr ng-repeat="book in books"> 
            <td ng-bind="book.title"></td> 
            <td ng-bind="book.author"></td> 
            <td ng-bind="book.isbn"></td> 
        </tr>
    </tbody>
</table>

As specified in the E2E test, we are expanding the list view in the form of an HTML table on (<table>). We also annotate this table with the bm-book-list CSS class. Now comes the interesting part with regard to the ngRepeat directive. As shown in Listing 3.19, we use the ngRepeat directive as an attribute to the HTML element that will be generated repeatedly as the output of the books array. The DOM subtree of the HTML element, including the HTML element itself, considers the directive a template that will be instantiated once for each element of the underlying array. So in this case, we want to ensure that a table row (<tr>) is rendered for every Book object in the books array, because we are using the directive with the <tr> tag.

The ngRepeat directive expects a structured expression, such as “book in books” in this case. The expression describes which array is to be iterated over and the name of the variable that can be used to access the array element from within the loop in each iteration. Here we specify that the array referenced by the scope variable books is to be iterated over and the loop variable book is how the loop can access the array element at each iteration. Thus, at each iteration we have access to the individual book details such as the book title (book.title), author (book.author) and ISBN (book.isbn). This relationship is illustrated again in Figure 3.7. You can also see in the diagram which DOM section in the scope of the BookListCtrl controller is valid.

Figure 3.7: ngRepeat iterates over a book array in the controller scope

Consequently, we can use the ngBind directive again to produce the actual output. The relationship between the ngRepeat directive and the repeater from the E2E test is shown in Figure 3.8.

One of the anomalies of ngRepeat is that the construct is also subject to two-way data binding. This means AngularJS automatically create new DOM elements at runtime when the content of the underlying array changes. In our case, this means we just need to add new Book objects in the books array at runtime to obtain a current list view of books.

Figure 3.8: The relationship between ngRepeat and the repeater

The second anomaly of ngRepeat is that it creates a new scope for each element of the array it is iterating over. This fact has no impact on our current List view example. However, it is important to know this background information to avoid any nasty surprises when creating more complex views. This aspect is especially appreciated when you render form fields using the ngRepeat directive. You should always make it clear in which scope you bind a field to a variable.

The fact that the ngRepeat directive creates a scope for each element of the underlying array leads, in the case of our application, to the list in Figure 3.9. To understand the aspect better, we have added the resulting HTML code from ngRepeat in the figure.

You can see in Figure 3.9 that AngularJS defines the scope variable book in each of the scopes produced by the ngRepeat directive. The scope variable book references the corresponding Book object.

To see the List view, direct the browser to the following URL.

http://localhost:8080/#/books

If you did not make a mistake, you should see the List view like that in Figure 3.10.

Figure 3.9: The scope hierarchy when displaying the List view

Figure 3.10: The List view

If you get the same output as that in Figure 3.10, then logically one of the three E2E tests will also be “green.” Finally, you can verify the correct number of books by using the familiar Karma call:

karma start karma-e2e.conf.js

Now only two tests are failing, as shown in the following output:

Chrome 30.0.1599: Executed 4 of 4 (2 FAILED) 
                                          (1.233 secs / 0.983 secs)

The test, which tests the number of books, should pass now. You can move on to make the other two tests “green.”

The orderBy Filter: Determining the Sort Order

As already mentioned, you can use the ngRepeat directive to sort an array based on certain criteria. You can use the orderBy filter that ships with AngularJS.

So let’s now extend our template for the List view, so our books will be sorted by book title in ascending order. We have formulated the corresponding E2E tests to meet this requirement. The extension can be seen in Listing 3.20.

Listing 3.20: Determining the sort order with the orderBy filter

<table class="bm-book-list"> 
    <tbody> 
        <tr ng-repeat="book in books | orderBy: 'title'"> 
            <td ng-bind="book.title"></td> 
            <td ng-bind="book.author"></td> 
            <td ng-bind="book.isbn"></td> 
        </tr> 
    </tbody> 
</table>

You can use a collection filter by expanding the structured ng-repeat expression accordingly. For this purpose, you use the pipe notation, just like you would a formatting filter. The pipe notation follows the name of the collection filter (here: orderBy). After the name comes a colon that separates the filter with a list of parameters for the filter. You can use static values as well as reference scope variables. As an alternative to the code in Listing 3.20, you could also define a variable named booksOrderedBy in the scope of the BookListCtrl controller and assign the string “title” to it. The resulting ng-repeat expression would then look like this:

ng-repeat="book in books | orderBy: booksOrderedBy"

Here, the orderBy filter is also subject to two-way data binding. This means that you just have to change the value of the scope variable booksOrderedBy at runtime to sort the books by a different property, such as the ISBN.

In particular, the orderBy filter takes two parameters. The first parameter is a comparison predicate, which you can specify in a couple of different ways.

  • If you pass a reference to a function defined in the currently valid scope, then this function is understood to be a getter function and its return value interpreted as a comparison value of each element. Sorting will then be based on the comparison value in each case, using comparison operator >, < or ==. The filter will pass the corresponding array element to the function, so you, in the case of an object, can easily have access to certain properties of the object and return them as reference values.
  • If you pass a string, this string is interpreted as an AngularJS expression and will finally be evaluated on each element of the array to determine the comparative value. Consequently, this specification of the comparison predicate makes sense only for arrays that contain objects. In addition, you can prefix it with a minus or plus sign to have the array elements sorted in ascending (+) or descending (-) order. Without a prefix, by default the elements will be sorted in ascending order.
  • You can specify an array of getter functions or expressions. You use the first predicate for comparison. If two elements of the array are equal, the next predicate is used to evaluate the two elements. If, after being compared with the second predicate, the two elements are still equal, the next predicate is used for comparison, and so on.

The second parameter to the orderBy filter is optional. If you pass true as the second parameter, the sorting done with the help of the first parameter will be reversed. If you do not supply a value, it is assumed sorting should not be reversed.

In our extended template in Listing 3.20, we use the second option for specifying the comparison predicate. We pass the string “title”, which indicates that sorting should be based on the title property of our Book objects.

Thus, we have specified sorting in the E2E Test with this small extension. As such, the remaining two test cases should also pass now. The manual version in the browser should display the correct order (see Figure 3.11).

Figure 3.11: Sorting the books in the List view

Next, we want to use the first option to specify a comparison predicate, to achieve the same effect of sorting. We do this by changing the ng-repeat expression as follows:

ng-repeat="book in books | orderBy: getBookOrder"

Now getBookOrder must be a variable in the currently valid scope and references a function that returns the comparison value for each Book object from the books array. The currently valid scope is still the scope of the BookListCtrl controller. This means we have to expand the implementation of this controller to that in Listing 3.21.

Listing 3.21: The BookListCtrl controller with the getBookOrder() function

bmApp.controller('BookListCtrl', function ($scope) { 
  $scope.books = [ 
    [...] 
  ];

  $scope.getBookOrder = function(book) { 
    return book.title; 
  };
});

Every time the getBookOrder() function is called, the orderBy filter passes the corresponding Book object as a parameter to the function. Because we want to sort by book title, we return the book title as a comparison value.

At this point that is all we have to do to define the comparison predicate using a getter function. To verify this solution to the sorting problem, we can rerun our E2E test suite.

Chrome 30.0.1599: Executed 4 of 4 SUCCESS (1.401 secs / 0.532 secs)

That’s it. All tests should pass.

Invoking A Filter Programmatically

So far we have only used the orderBy filter with the ngRepeat directive. This is a common way to use the filter. However, you might also want to call a filter programmatically in order to use it within a controller or a service as an auxiliary function. The good news is you can do this in AngularJS. Let’s look at how to do it.

Listing 3.22: Calling the orderBy filter programmatically in the BookListCtrl controller

bmApp.controller('BookListCtrl', function ($scope, $filter) { 
  $scope.books = [ 
    [...] 
  ];

  $scope.getBookOrder = function(book) {
    return book.title; 
  };

  // This is just to demonstrate the programmatic usage of a filter 
  var orderBy = $filter('orderBy'), 

  var titles = $scope.books.map(function(book) { 
    return {title: book.title}; 
  });

  console.log('titles before ordering', titles); 

  // This is the actual invocation of the filter 
  titles = orderBy(titles, 'title'), 

  console.log('titles after ordering', titles); 
});

As shown in Listing 3.22, to invoke a filter programmatically you use the $filter service, which AngularJS can pass to you via dependency injection. To achieve this, add a parameter named $filter to the constructor. This is how you tell AngularJS that you need the $filter service in the constructor.

The $filter service consists solely of a function that returns a reference to the filter function of this filter, which you can obtain by passing the filter name to the function. What we do in Listing 3.22 is call $filter('orderBy') to get the reference to the filter function of the orderBy filter. We store this reference in a local variable orderBy. We can now call the logic in the orderBy filter with orderBy().

To see a programmatic call in action, we define a new array named titles and populates it with data from our books arrays using the Array.map() function. The new array contains only book titles. At first the elements in this array are in the same order as the Book objects in books. After we apply the filter function of the orderBy filter on this array, however, we get a new array containing titles in ascending order.

A filter invoked programmatically expects the same parameters as when it is used in a template. However, for a collection filter, as in this case, we must also pass as the first parameter the array on which the filter will apply.

For comparison, we show the titles array before and after sorting in Figure 3.12.

Figure 3.12: The console output of the sorted array

Summary

  • You can sort a collection such as an array with the orderBy filter.
  • The filter filter is a collection filter and comes with the template associated with the ngRepeat directive.
  • Used in a template:
      ng_repeat_expression | orderBy:predicate[:reverse]
  • Invoked programmatically using the $filter service:
      $filter('orderBy')(array, predicate [, reverse]) 
  • As a predicate you can define a function, a string or an array to sort according to the options described above.
  • You can reverse the sorting order by passing true to the second parameter. The second parameter is optional.

Filtering Data

After looking at the orderBy filter in the previous section, we will now go one step further and implement a function to filter entries for our List view. We will use another filter filter. With the filter filter, we can restrict the output of the underlying array based on certain criteria.

However, before we write the actual implementation, we will implement the test cases that specify the expected behavior for our filter function. For this purpose, we extend the E2E test suite for our List view and write three more test cases. This extension is shown in Listing 3.23.

Listing 3.23: The E2E tests for the search function

describe("E2E: book list view", function () { 

    [...] 

    it('should allow filtering by book title', function() { 
        // Coffee 
        var searchText = orderedTitles[0].substr(0, 6); 
        input('searchText').enter(searchText);
        expect( 
            repeater(selector).column('book.title')
        ).toEqual([orderedTitles[0]]); 
    });

    it('should allow filtering by author', function() { 
        // Andreas 
        var searchText = expectedBooks[0].author.substr(0, 7); 
        input('searchText').enter(searchText); 
        expect( 
            repeater(selector).column('book.title')
        ).toEqual([orderedTitles[0]]); 
    });

    it('should allow filtering by isbn’, function() {
        // 050-1 
        var searchText = expectedBooks[0].isbn.substr(-5, 5); 
        input('searchText').enter(searchText); 
        expect( 
            repeater(selector).column('book.title')
        ).toEqual([orderedTitles[0]]); 
    });
});

In order to formulate the actual expectation, in the three test cases we use the same mechanism that we have seen in the previous sections, namely a repeater. However, unlike the previous test cases, in the new test cases we use the input() function of ngScenario. Using this function you can reference a DOM element that is defined in the template using an <input> tag. This means you can use the input() function to interact with, say, input fields, radio buttons and checkboxes. Therefore, the input() function expects as a parameter the name of a scope variable to which the <input> element is bound. In the implementation of the filter function we can bind the <input> tag to a scope variable using the ngModel directive. So using the input() function we specify that in our List view that there must be a listbox bound to the scope variable searchText by the ngModel directive. This input field is used to enter a search string, based on which the entries are to be filtered in the List view.

You can use enter() to enter a string into this box programmatically. You pass the string as the first parameter to the enter() function. If you take a closer look at the three new test cases, you will see that they all follow the same pattern. We let the execution environment enter a string in the input field and then inspect if the book list contains only the correct entries. It should be possible to do a full text search on all properties of a book. In each test case we have randomly defined a filter for a book title, an author and an ISBN, respectively. Thus, the filtered List view after the input “Coffee” in Test Case 1, “Andreas” in Test Case 2 and “050-1” in Test Case 3” should always lead to the same result, that is, only one entry should be displayed: the “CoffeeScript” book by Andreas Schubert.

Naturally, the execution of our E2E Test Suite for the List view should now lead to three unsuccessful tests.

Chrome 30.0.1599: Executed 7 of 7 (3 FAILED) 
                                         (1.479 secs / 1.186 secs)

So we can now work to meet the test case expectations. We begin by expanding the template for the List view to the input field we just mentioned for the search function (See Listing 3.24).

Listing 3.24: The definition of the input field for the search function

<input type="text" placeholder="Search..." ng-model="searchText"> 
<table class="bm-book-list">
  <tbody> 
    <tr ng-repeat="book in books | orderBy: getBookOrder"> 
      <td ng-bind="book.title"></td> 
      <td ng-bind="book.author"></td> 
      <td ng-bind="book.isbn"></td> 
    </tr> 
  </tbody> 
</table>

The interesting thing here is the ngModel directive used with the <input> tag. It ensures that two-way data binding is established between the box and the searchText scope variable. We reference the searchText variable in the scope that is valid for the DOM section of the List view. That is to be the scope of our BookListCtrl controller. We have not chosen the name for the scope variable searchText arbitrarily. We know that in our test cases, we have specified that the input field should be bound to a scope variable named searchText.

We have seen the ngModel directive in action in Chapter 1, “AngularJS Quick Start.” With two-way data binding AngularJS automatically updates the searchText scope variable when the input field’s value changes. The same applies in the opposite direction. Therefore, a change in the value of the scope variable searchText will be visible immediately in the input field. For our filter function only the first aspect of the two-way data binding is relevant.

If we have not made a mistake, the search box should now appear on our book list (See Figure 3.13). However, the entries are not yet restricted to the list because we have not installed the filter filter.

We can now introduce the filter filter, as shown in Listing 3.25.

Figure 3.13: The search field in the List view

Listing 3.25: The search function with the filter filter

<input type="text" placeholder="Search..." ng-model="searchText"> 
<table class="bm-book-list"> 
  <tbody> 
    <tr ng-repeat="book in books | orderBy: getBookOrder 
     | filter: searchText"> 
      <td ng-bind="book.title"></td> 
      <td ng-bind="book.author"></td>
      <td ng-bind="book.isbn"></td> 
    </tr> 
  </tbody> 
</table>

Since the filter filter is a collection filter, just like the orderBy filter, we use it with the ngRepeat directive. For this purpose, we extend the ng-repeat expression with the pipe notation.

As mentioned previously, the searchText scope variable serves as a filter predicate for the filter. Therefore, we pass the value of this variable as the first parameter to the filter. This way, the searchText variable created has a connection from the box to the filter filter. As a result, the filter restricts the book list based on the input. AngularJS performs the filter logic so that every time the value of searchText changes, you immediately get a filtered list that represents a full text search result on the array of Book objects.

If you run the example in the browser and enter, for instance, the string “coffee” in the input field, you will get the expected effect immediately, as shown in Figure 3.14. Our List view should contain only the entry with the “CoffeeScript” book by Andreas Schubert, because this is the only book that contains the string “Coffee” in one of its properties. We should emphasize at this point that we implemented the search function without writing a single line of JavaScript code.

Figure 3.14: The filtered List view

The tests we created must now run successfully. The result from Karma with our configuration for the E2E tests is as follows.

Chrome 30.0.1599: Executed 7 of 7 SUCCESS (1.763 secs / 1.474 secs)

Before we discuss the operation of the filter filter in more detail, we can see in Figure 3.15 the relationship between the ngModel directive, the filter filter and the input() function in the E2E test.

Figure 3.15: The relationship between ngModel, filter and input()

In Listing 3.25 the filter filter filters the array with respect to any property of the Book object that contains a string or can be converted to a string. A match is therefore determined based on the comparison of substrings without case distinction. This means this type of filter use is equivalent to a full-text search.

Like the orderBy filter, you can configure the filter filter further by passing a filter predicate instead of a string or a function. Thus, there are the following possibilities.

  • If you pass a string as a filter predicate (as in Listing 3.25), then the filter will filter the underlying array by comparing each array element with the filter predicate. An element is included in the resulting array if it matches the string or any of its substrings when it is compared to the string without case sensitivity. If the array elements are objects (such as in the case of the books array), all the string properties of the objects and all properties that can be converted to string are compared. In addition, you can negate the filter predicate by prefixing it with the exclamation mark (“!”).
  • If the elements of the underlying array are objects (such as our books array), you can use the filter predicate to determine which object properties should be checked for a match. You can also specify a separate search string for each object property. Example:
      book in books | filter: {title: 'Coffee', author: 'Andreas'} 

In this case, only books that meet the two conditions will be included in the result, books whose title contains the string “coffee” and whose author contains the string “Andreas”. Since the comparison is made without regard to case-sensitivity, the values “coffee” and “andreas” would also be a match.

  • If you pass a function as the filter predicate, you can specify any filtering logic in the function. AngularJS calls this function for each element in the array when the filter is being evaluated. The framework passes the corresponding element to the function as the first parameter. The convention is that this function must return true if the element is to be included in the resulting array. Otherwise, it must return false. Example:
      book in books | filter: getCoffeeBook

In the controller’s constructor function, we now have to ensure that the getCoffeeBook() function is defined in scope and contains the appropriate filtering logic.

      $scope.getCoffeeBook = function(book) { 
          return book.title.indexOf('Coffee') !== -1; 
      };

In this case, only books that contain the string “Coffee” will be included in the result. Since we are using indexOf(), the string “coffee” will no longer produce a match.

In addition to the filter predicate that passed as the first parameter, you can pass an optional second parameter to the filter filter. Using the second parameter you can specify a comparator. As the name suggests, a comparator is a function that defines logic to compare the filter predicate with the values from the array. It means you can use the comparator to determine whether or not string comparison should be case sensitive or define a different rule entirely. With regard to the comparator, you have a couple of options:

  • If you pass a comparator function, the framework always calls this function when a comparison between a value from the array and the filter predicate needs to be performed. You obtain the filter predicate as the first parameter and the array value as the second parameter. The rule is that the function must return true if there is a match and false if no match was found.
  • You can simply pass true to indicate comparison will be based on angular.equals(). This means case sensitivity is taken into account in comparison and an exact match, not just a substring match, is required for a match.

Calling the filter Filter Programmatically

Just like other AngularJS filters, the filter filter can be invoked programmatically in any application component (such as a controller, service and directive). For this you use the $filter service, which the framework can pass to you through dependency injection. The actual call would look like this.

$filter('filter')(array, predicate[, comparator]);

Again, you need to pass the array to be filtered as the first parameter, before you pass the filter predicate and a comparator as the second and optional third parameter.

Summary

  • Using the filter filter you can filter an array based on certain criteria. For example, you can implement a client-side search feature.
  • In addition to supporting full text search, you can control thoroughly how a data collection will be filtered.
  • With an optional comparator you can define filtering logic for the compare operation.
  • Used in a template:
      ng_repeat_expression | filter:predicate[:comparator]

  • Invoked programmatically with the $filter service:
      $filter('filter')(array, predicate[, comparator]);

Navigating within the Application

We now have a rudimentary Details view and an implementation of a List view. However, so far there is no link between these two views. It means you currently can only navigate between these two views using the browser address bar. In this step we want to change that and show how we can implement navigation in AngularJS. For this purpose, we formulate this user story.

The Default Route with $routeProvider.otherwise()

Recall that in the app.js file we configured a route for the two views. Before we establish the link between them, we first want to optimize something in our route configuration. With an otherwise() block we can specify what should happen when the user navigates to a URL that is not covered by the when() blocks. Using the redirectTo property we state that such a call should be redirected to the /books route, which ultimately brings the user to our List view. The extension is printed in Listing 3.26.

Listing 3.26: Extending the route configuration (app.js) with an otherwise() block

[...] 

bmApp.config(function ($routeProvider) { 
  $routeProvider.when('/books/:isbn', { 
    templateUrl: 'templates/book_details.html', 
    controller: 'BookDetailsCtrl'
  }) 
  .when('/books', { 
    templateUrl: 'templates/book_list.html', 
    controller: 'BookListCtrl'
  })
  .otherwise({ 
    redirectTo: '/books' 
  });
});

Now we want to establish a link between the List view and the Details view by implementing a mechanism to call the Details view of a particular book from the List view. Of course, there should also be a way to navigate from the Details view back to the List view.

First, the Test

Before we start the actual implementation, we would like to specify the required behavior in the form of test cases. To do this, we need to extend the test suite for the List view (book_list.spec.js) as well as the test suite for the Details view (book_details.spec.js) by one test case.

Listing 3.27: The test case for navigating from the List view to the Details view

describe("E2E: book list view", function () { 

    [...] 

    it('should appropriately navigate to details view', function() { 
        var i = 0, 
            detailsLink = selector + ':nth-child('+ (i+1) +') a';
        element(detailsLink).click(); 

        expect( 
            browser().location().path() 
        ).toEqual('/books/' + expectedBooks[i].isbn); 
    });

});

Listing 3.27 shows a test case for the List view. This test case shows some aspects of ngScenario that you have not encountered before. First, here we use the element() function to reference a DOM element using a CSS selector. Second, it shows that you can obtain certain information of the current status of the address bar using browser().location().

We extend our original selector CSS selector so that we reference the <a> tag in the nth line that contains the hyperlink to the corresponding Details view. Next, we store the resulting selector in a local variable called detailsLink, which in this test case is restricted to the <a> tag in the first line (var i = 0). We have therefore specified that the link from the List view to the Details view should be created through a hyperlink. By calling the click() function of the referenced DOM element, we tell Karma to click on the corresponding DOM element upon the execution of the test case. Our application should respond to that click by opening the corresponding Details view. We can check this behavior easily by examining the content of the address bar. In line with our route configuration, the path component of the URL must match /books/:isbn. The isbn part of course corresponds to the ISBN of the book whose details we want to view. We formulate this expectation using expect(). We can access the path component of the current URL using browser().location().

We now have a link from the List view to the Details view with the test case in Listing 3.27. Now we need a test case that specifies a way back from the Details view to the List view. We define this test case in Listing 3.28.

Listing 3.28: The test case to navigate from the Details view to the List view

describe("E2E: book details view", function() { 

    [...] 

    it('should appropriately navigate to list view',
    function() { 
        browser().navigateTo('#/books/978-3-89864-728-1'), 

        element('.bm-list-view-btn').click();
        expect( 
            browser().location().path() 
        ).toEqual('/books'), 
    }); 
});

As shown in Listing 3.28, the test case for navigating back from the Details view to the List view has the same structure as the previous test case. We navigate to the detailed description of the book with the ISBN 978-3-89864-728-1 and let karma click on the DOM element, which we refer to using the .bm-list-view-btn CSS selector. Then, we expect our application to display the List view. We examine this aspect by comparing the value of the browser’s address bar with the path component of the URL.

After we extended the two test suites, we can run our E2E tests with the E2E configuration and make sure they fail. We can then go ahead and implement the required features to eventually meet the user story.

Navigating with Hashbang URLs

To add a link to the Details view, we can easily expand the template for the List view by leveraging the ngHref directive. We used this directive at the beginning of the BookMonkey project to place a hyperlink to the publisher website in the Details view.

We need to use the ngHref directive with an <a> tag to define a hyperlink that contains one or more expressions. The reason for this is AngularJS uses this directive to make sure that the link is only set when the expression has been evaluated. If we do not use this directive, the user may see a link that is invalid because the framework has not yet evaluated the expression. Since AngularJS usually evaluates expressions pretty fast, the probability of displaying an invalid link is quite small, but not zero.

Therefore, we use the ngHref directive and an <a> tag to link the book title in the List view with the corresponding Details view. We do this by setting the ng-href directive with the relative path to the Details view (see Listing 3.29). Using the expression {{book.isbn}} we ensure that we use the correct ISBN for each book. However, we need a hash key (#) before the relative path, so that older browsers will not reload the web page completely with this URL.

We will now talk about hashbang URLs. we practically use the HTML anchor mechanism to display a different part of our application. AngularJS continuously monitors the content of the address bar and loads any change in the corresponding view.

To make various areas (List view, Details view, and other views later) available in our application by deep linking, we use hashbang URLs here. These are URLs where we code to invoke an application state mainly in the hash portion of the URL (the part after the hash). In essence, we use hashbang URLs to prevent older browsers to reload the page when the user clicks on a link that should load another part of the application.

In addition to the hashbang approach, which admittedly feels like an interim solution and also makes deep links look ugly, we can use the HTML5 History API. Using this API, it is possible to work with regular URLs and still prevent a complete reload of the web application. In AngularJS you can activate the so-called HTML5 mode for your application.

By activating the HTML5 mode, the framework will use hashbang URLs for navigation in older browsers and regular URLs with the HTML5 History API in modern browsers. To accomplish this, AngularJS rewrite all links accordingly at runtime.

However, there is a drawback when using the HTML5 mode, which is why by default it is disabled. If you enable HTML5 mode for your application, you also need to describe any deep links on the web server that delivers your application. Finally, you have to ensure that every deep link used to invoke the application will produce the requested client-side application state as soon as it arrives at the browser for execution.

You can enable the HTML5 mode by using the $locationProvider component within a configuration block (config()). Here is the actual call:

$locationProvider.html5Mode(true)

Listing 3.29: The template for the List view with a hyperlink to call the Details view

<input type="text" placeholder="Search..." 
        ng-model="searchText"> 
<table class="bm-book-list"> 
    <tbody> 
        <tr ng-repeat="book in books | orderBy: getBookOrder 
                | filter: searchText"> 
            <td><a ng-href="#/books/{{ book.isbn }}" 
                ng-bind="book.title"></a></td> 
            <td ng-bind="book.author"></td> 
            <td ng-bind="book.isbn"></td> 
        </tr> 
    </tbody> 
</table>

With the expansion in Listing 3.29 we have a link from the List view to the Details view. Executing the code in the browser should show the expected result and our book titles should be rendered as links (see Figure 3.16). As such, one of the tests we defined should now pass.

Figure 3.16: The List view with book titles as links

What is still missing is a link on the Details view to go back to the List view. To add the link, we need an <a> tag. However, we do not need the ngHref directive, because the relative path to the List view contains no expression. It is sufficient to provide the link with the ordinary href attribute.

However, there is one aspect that we have to consider. In the E2E test we reference the DOM element to be clicked using the .bm-list-view-btn CSS selector. This means that we need to annotate our Back link with a bm-list-view-btn CSS class so that Karma can reference the DOM element in the E2E test. The necessary changes can be seen in Listing 30.3.

Listing 3.30: The template for the Details view with a Back link to the List view

<h2 ng-bind="book.title" class="bm-book-title"></h2> 
<h3 ng-bind="book.subtitle" class="bm-book-subtitle"></h3> 

[...] 

<a href="#/books" class="bm-list-view-btn">Back</a>

We have now implemented the required navigation. If we run our E2E test suites again, the two test cases created earlier should no longer fail. Also, the manual test in the browser should reflect the required behavior. Clicking on a book title on the List view should bring us to the Details view. To go back to the List view, we can click on the “Back” link.

Figure 3.17: The Details view with a Back link

Of course in the Details view you still only see the details of the book “JavaScript for Enterprise Developers,” because we do not use the ISBN that was passed via the URL in the BookDetailsCtrl controller (See Figure 3.17). We will take care of this in the next section when we introduce our first service.

The ngClick Directive: Responding to the Click Event

So far, so good. Nevertheless, we want to handle the implementation of the Back link in the Details view again to introduce two more interesting features of AngularJS, the ngClick directive and the $location service.

With the ngClick directive we can determine which function to call when the user clicks on a DOM element. Thus, it seems logical to implement the Back link using this directive, even though doing so will not bring any advantage other than for educational purposes.

Listing 3.31: Using the ngClick directive to call a function from the controller scope

<h2 ng-bind="book.title" class="bm-book-title"></h2> 
<h3 ng-bind="book.subtitle" class="bm-book-subtitle"></h3> 

[...] 

<a ng-click="goToListView()" href="" 
  class="bm-list-view-btn">Back</a>

As can be seen in Listing 3.31, we use the ngClick directive to specify that the goToListView() function be called when the user clicks on the <a> element. AngularJS therefore expects the goToListView() function to be defined in the currently valid scope of the Details view or a parent scope. The currently valid scope for the Details view is still the scope of the BookDetailsCtrl controller. Therefore, we will implement this feature in the same scope as this controller.

You may have noticed that our <a> tag contains an href attribute that is assigned an empty string. The only reason for this is that most browsers only format a link if the <a> tag contains a href attribute. Therefore, we simply define an empty href attribute to get around the CSS rule.

The $location Service: Interacting with the Address Bar

We can now proceed and implement the goToListView() function in the scope of the BookDetailsCtrl controller. See Listing 3.32.

Listing 3.32: The goToListView() function in the BookDetailsCtrl controller

bmApp.controller('BookDetailsCtrl', 
function ($scope, $location) { 

    [...]

    $scope.goToListView = function() { 
        $location.path('/books'), 
    }; 
});

As you can see in Listing 3.32, the implementation of goToListView() is simple and consists of exactly one line of JavaScript code. The important thing to note is that we can get AngularJS to pass us the $location service through dependency injection. We do this by adding a parameter in the controller’s anonymous constructor.

The $location service allows you to interact with the browser address bar easily. Internally, this service uses the window.location object of the DOM API and provides a kind of two-way data binding. This means any changes that you make with the $location service have a direct impact on the address line and vice versa. Thus, you can programmatically change the content of the address bar and navigate to another view within the application using this service easily.

That's what we do in the goToListView() function. Using the path() function we manipulate the path component of the current URL and assign /books to it. This means that when you click on the Back link in the Details view, the /books route is called, which causes the List view to be displayed. This behavior corresponds exactly to our first choice. Thus, the corresponding E2E test should still pass.

In addition to the path() function, the $location service provides a few more functions to interact with the address bar. A complete listing and description can be found here:

http://docs.angularjs.org/api/ng.$location

We should point out that the service API is structured so that you can use the functions to write to as well as read from the various components of the URL. Thus, the call to $location.path() (without a parameter) would return the path component of the currently loaded URL.

Summary

  • You can use $routeProvider.otherwise() in the route configuration to specify a default route that will be used when there is no match with any of the defined when() routes.
  • You can use element () to access a DOM element in an E2E test, by passing a CSS selector to the function. You can use click() to programmatically perform a mouse click on this element.
  • You can use the browser().location() object and its numerous functions such as path() or hash() in an E2E test to collect certain information about the current status of the address bar.
  • Within our AngularJS application we usually navigate between different parts of the application to which we have defined a route using hashbang URLs.
  • Hashbang URLs also allows routes to be specified as deep links.
  • For ordinary link, you use the <a> tag and the href attribute. If a link contains an expression, resort to the ngHref directive.
  • You can switch to another application part programmatically. You can use the $location service of AngularJS to easily interact with the browser address bar.
  • You can also use the HTML5 mode to force AngularJS to establish internal linking to the application with regular URLs. However, this mode requires an adjustment to the web server that delivers the application.
  • Using the ngClick directive you can define in a template which function should be executed when the user clicks on the annotated DOM element.

The First Service

After implementing two views for the BookMonkey application and having shown in the previous chapter that you can establish a link between individual parts of the application, we now want to create the first service in this section. We want to fix the problem with the Details view always showing the information about the “JavaScript for Enterprise Developers” book because the passed ISBN is not being evaluated in the BookDetailsCtrl controller. The service we will create is called BookDataService.

As you have learned in Chapter 2, “Basic Principles and Concepts,” you can use a service within an AngularJS application to perform a number of tasks, including accessing application data. Therefore, we can use a service to create a data access object, which is what our BookDataService will be designed to do.

An essential characteristic of services is that it provides a clear and stable API. It is important because with a clear and stable API you can replace the internal implementation of a service and offer overall flexibility.

For the feature we will implement in this section, we write this user story:

First, the Test

Like before, we start with the tests and we will write unit tests for the first time. At the beginning of the BookMonkey project, we created a directory named unit under the test directory. Now in the unit directory we need create a directory called services, and in this directory we will create a book_data.spec.js file that will contain the unit tests for BookDataService.

As already mentioned, AngularJS is fundamentally designed for testability. Thus, all application components can be tested with unit tests. So far we have only tested the user interaction with the views and thus more or less considered our application a black box. Our E2E tests are also made for this purpose. However, for the first time it makes sense to write unit tests to test BookDataService.

In Listing 3.33, you can see that the basic structure for unit testing looks the same as the structure for the E2E testing. However, we should make it clear from the beginning that we will use another testing framework for unit testing and therefore have to employ a different test API. For our E2E tests we have used ngScenario that is part of AngularJS. For unit testing, however, we will use Jasmine. Purely syntactically, the two frameworks are similar, but functionally they are designed for different purposes. As with ngScenario, where you can write E2E tests and practically define specific user behavior and then check rather superficial expectations based on the DOM, with Jasmine you can call and unit test all features of a component and make precise statements about the component state. Because of this, you can formulate very precise expectations.

AngularJS comes with the ngMock module to help you write Jasmine unit tests. This module offers an API that allows you to download and reference your application modules and components. Thus, you have full access to all functions that are provided by the components. Another great advantage is that the dependency injection mechanism of AngularJS can also be used in unit tests. This means that you can replace dependencies with predefined mock objects in tests. The ngMock module even comes with some mock implementations to simplify the creation of isolated unit tests.

Listing 3.33: The beforeEach() block of the unit tests for BookDataService

describe('Service: BookDataService', function () { 
   var BookDataService; 

    // load the application module 
    beforeEach(module('bmApp')); 
    // get a reference to the service 

    beforeEach(inject(function (_BookDataService_) { 
        BookDataService = _BookDataService_; 
    })); 

    [...] 

});

Listing 3.33 shows the first steps of our unit tests for BookDataService. As with our E2E tests, we use the describe() function to define a test suite for the unit tests. This test suite contains two beforeEach() calls that define the functions to be performed before each unit test. In these beforeEach() calls we can even see two main functions, module() and inject(), in action. These functions are provided by the ngMock module.

With the module() function we will load our bmApp application module for the test. This ensures that we can access all components (services, directives, controllers etc) that have been implemented within this module.

The inject() function is largely responsible for making sure you can access the components of a module after you load the module. As the name implies, you can use this function to tell AngularJS to inject application components through dependency injection. This basically works by following the same pattern as the anonymous constructor of our components. The inject() function takes a function as a parameter, and within that function you can specify in the parameters which application modules should be provided by the framework. As you can see in Listing 3.33, these are still a small feature.

We want AngularJS to inject BookDataService because we want to test the functions in this service in the test. It is good practice to store the reference to the service object in a local variable that has the same name, so you can easily access the service in the unit tests. Here we save the passed reference in a local variable named BookDataService. We declare this variable at the beginning of our test suite (var BookDataService). Now, however, we have a problem.

According to dependency injection rules, dependency resolution is based on parameter names. Unfortunately, we cannot call the parameter in the second beforeEach() call BookDataService, because we would overshadow our local BookDataService variable. Fortunately, the inject() function allows us to enclose the variable name with underscores (_BookDataService_). The AngularJS injector, the internal component that resolves dependencies, knows that in tests it should ignore underscores when resolving dependencies. Consequently, we can assign our local variable BookDataservice the reference to the service object, to make it available for the following unit tests.

Now that we have created the basic structure of the test suite, we can continue with the first unit tests for BookDataService.

As shown in Listing 3.34, it is entirely possible to nest describe() blocks and thus structure test suites a bit finer. The beforeEach() calls that we define in the outer describe() block to get a reference to our BookDataService, apply equally to all test cases defined within the inner describe() block. We should mention at this point that describe() blocks can nest to any arbitrary depth and that they may also contain beforeEach() and afterEach() blocks, which are executed before and after each inner test case.

Listing 3.34: Unit tests for verifying the public API definition

describe('Service: BookDataService', function () { 

    [...] 

    describe('Public API', function() { 
        it('should include a getBookByIsbn() function', function () 
        { 
            expect(BookDataService.getBookByIsbn).toBeDefined(); 
        }); 

        it('should include a getBooks() function', function () { 
            expect(BookDataService.getBooks).toBeDefined(); 
        }); 
    });

    [...] 

});

First, we verify the definition of the public interface of our BookDataService. In the first step, we demand that the service provide two functions, getBookByIsbn() and getBooks(). To this end, we define the corresponding test cases with it() in a separate sub-suite.

Just like ngScenario, Jasmine ships with a beautiful collection of matchers you can use to conveniently express expectations. In Listing 3.34 we use toBeDefined(), a matcher, to ensure the presence of the two API functions (getBookByIsbn() and getBooks()).

Since we have ensured that the public API of our service is offering the getBookByIsbn() and getBooks() functions, we can now go ahead and write the implementations of these functions. For this we define a separate suite (See Listing 3.35), which in turn has two sub-suites, each for each function. From this example we can see how easily we can structure our unit tests within a file.

Listing 3.35: The unit tests for checking the functionality of the public API

describe('Service: BookDataService', function () { 

    [...] 

    describe('Public API usage', function() { 
        describe('getBookByIsbn()', function() { 
            it('should return the proper book object (valid isbn)',
            function() { 
                var isbn = '978-3-86490-050-1',
                    book = BookDataService.getBookByIsbn(isbn); 
                expect(book.title).toBe('CoffeeScript'), 
            });

            it('should return null (invalid isbn)',
            function() { 
                var isbn = 'test',
                    book = BookDataService.getBookByIsbn(isbn); 
                expect(book).toBeNull(); 
            });
        });

        describe('getBooks()', function() { 
            it('should return a proper array of book objects',
            function() { 
                var books = BookDataService.getBooks(); 
                expect(books.length).toBe(3); 
            });
        });
    });

});

What the two API functions should do is already apparent from the names. The getBookByIsbn() function returns the Book object that matches the passed ISBN, whereas getBooks() returns an array of all existing books.

The corresponding unit tests are implemented in Listing 3.35. We expect the getBookByIsbn() function to return a Book object with the title “CoffeeScript” when we call it by passing the ISBN 978-3-86490-050-1. We check it with the toBe() matcher. If no book is associated with the passed ISBN, the function should return null, which we test in the second test case using the toBeNull() matcher.

The third test case verifies that the getBooks() function returns an array with three elements. We have not entered more books in BookMonkey. In this test case, we could certainly implement more logic to check whether the returned object is actually an array, and if the corresponding elements have the structure of a Book object (keyword duck typing). We have kept the tests simple because we just want to explain the basic idea and not dive into the details.

We have therefore formulated our expectations of BookDataService in the form of unit tests. We can run the newly created test suite once, to ensure that the test cases fail. After all, we have not implemented the BookDataService service.

We also have to note that we are now using our Karma configuration for unit testing. This configuration is in the karma.conf.js file and should also be in the root directory of our project. If you have not done so, you should copy this configuration from the root directory of the downloaded zip file.

Here is the command to invoke Karma to run the unit tests with the configuration file.

karma start karma.conf.js

If you have not made a mistake, Karma should give you information about the failed unit tests.

Chrome 30.0.1599: Executed 5 of 5 (5 FAILED) ERROR 
                                          (0.727 secs / 0.022 secs)

In addition to the summarized output, for each test case we get a detailed stack trace and an explanation of why the test case failed. It may be because we formulated an expectation incorrectly or because of an actual error in the program flow. In our case, each of the five test cases should fail for the same reason.

Error: Unknown provider: BookDataServiceProvider <- BookDataService

This error message means that the AngularJS injector could not find our BookDataService and thus the inject() function could not resolve the dependency. This is quite logical, because we have not yet implemented the service.

BookDataService: Encapsulating Data Access

We can now go ahead and write the first implementation of BookDataService. We first create a directory called services in the /app/scripts directory. We then create a book_data.js file in this directory and implement BookDataService.

Listing 3.36: The public API of BookDataService

bmApp.factory('BookDataService', function() { 

    [...] 

    // Public API 
    return { 
        getBookByIsbn: function(isbn) { 
            [...] 
        },
        getBooks: function() { 
            [...] 
        } 
    }; 
});

As you have learned in Chapter 2, “Basic Principles and Concepts,” AngularJS comes with several services. In Listing 3.36, you can see that you use the service factory (the factory() function) for BookDataService. A service provider has the advantage over the factory, which can be configured in a config() block. However, our BookDataService does not need configuration and we can save time writing a provider. Another possible alternative would be to use the service() function to register a class instance as a service. In addition, we have decided not to make the example unnecessarily complex. The service factory is the most common way to create a service.

Listing 3.36 shows that in the first step we set the public interface of BookDataService according to our unit tests. We usually return a so-called API object in the anonymous function that defines the service. We define all functions that belong to the public API of the service in this object. These are all the features that we want to provide to other components. As specified in the unit tests before, the first step should be in both getBookByIsbn() and getBooks() functions.

Having defined the object that the API will return, we can now take care of the implementation. In the first step we will store the static book data within the service. Before we tackle this step, we should create a local namespace for our implementation. To this end, we create a new object and assign it to a local variable named srv.

It is good practice to write the complete internal implementation in a namespace, so you do not accidentally pollute the global namespace with internal functions, which can lead to hard to identify errors if other parts of the application call these functions by mistake.

After we have created our local namespace object srv, we define within this object an array named _books. This array will eventually contain Book objects (see Listing 3.37). Because the variable name is prefixed with an underscore, the array is only intended for internal use and is therefore not published externally. The srv._books array is therefore the first step in our internal data structure used for managing Book objects.

Listing 3.37: The internal data structure of BookDataService

bmApp.factory('BookDataService', function() { 
    var srv = {}; 

    srv._books = [ 
        { 
            title: 'JavaScript for Enterprise Developers', 
            [...] 
        }, 
        { 
            title: 'Node.js & Co.', 
            [...] 
        },
        {
            title: 'CoffeeScript',
            [...] 
        }
    ];

    // Public API 
    return { 
        [...] 
    }; 
});

We can now implement the getBookByIsbn() function (See Listing 3.38). We declare srv in our namespace again. The implementation is relatively simple. We iterate over all books in the srv._books array and compare the ISBN of each book with the function parameter. If the book with the given ISBN is found, we return a copy of the corresponding object. We return a copy so that any reference to objects in our internal data structure is not exposed to the outside. Finally, we want to have control over the changes to the data structure in our service.

If no book with the given ISBN was found, we return null. Recall that we have specified this in the unit test. Finally, we need to establish a connection between our API object and the internal implementation. For this purpose, we delegate calls to our API (getBookByIsbn()) to our internal implementation (srv.getBookByIsbn()). JavaScript expert will notice immediately that this connection causes the getBookByIsbn() API function to turn to a closure and the references to the srv.getBookByIsbn() function to “includes” so srv.getBookByIsbn() will not be cleared after the return of the API object from the runtime environment.

Listing 3.38: The internal implementation of the getBookByIsbn() function

bmApp.factory('BookDataService', function() { 
    var srv = {}; 

    [...] 

    // Service implementation 
    srv.getBookByIsbn = function(isbn) { 
        for (var i = 0, n = srv._books.length; i < n; i++) { 
            if (isbn === srv._books[i].isbn) { 
                return angular.copy(srv._books[i]); 
            }
        }
        return null; 
    };

    [...] 

    // Public API 
    return { 
        getBookByIsbn: function(isbn) { 
            return srv.getBookByIsbn(isbn); 
        },
        [...] 
    };
});

Next, we implement getBooks(), the second API function, also within our srv namespace (See Listing 3.39). The implementation is quite simple. Since we do not want to expose references to our internal data structure srv._books, we copy the array before returning it. Thus, we do not have to worry that a potential caller might obtain the reference to our data structure and manipulate it. As before, we have to delegate calls to the API getBooks() to the srv.getBooks() internal implementation function. In this case, the JavaScript runtime environment will not clear srv.getBooks() after returning the API object, because the getBooks() closure includes a reference to this implementation function.

Listing 3.39: The internal implementation of the getBooks() function

bmApp.factory('BookDataService', function() { 
    var srv = {}; 

    [...] 

    // Service implementation 
    [...] 

    srv.getBooks = function() { 
        // Copy the array in order not to expose 
        // the internal data structure 
        return angular.copy(srv._books); 
    };

    // Public API 
    return { 
        [...] 
        getBooks: function() { 
            return srv.getBooks(); 
        }
    };
});

So at this point we have implemented the first version of our BookDataService. Consequently, the previously defined unit tests should pass. To prove this, call karma again with our configuration for unit testing.

karma start karma.conf.js

You should get the following feedback.

Chrome 31.0.1650: Executed 5 of 5 SUCCESS (0.892 secs / 0.016 secs)

Integrating BookDataService

Now we can integrate BookDataService in our two controllers BookListCtrl and BookDetailsCtrl. First, we take the BookListCtrl controller (See Listing 3.40). For this purpose, we tell AngularJS to pass us BookDataService via dependency injection, by specifying it as the third parameter to the controller’s anonymous constructor. Afterward, we can use it inside the controller service.

Instead of the scope variable books that references the locally defined array of Book objects, we use the getBooks() API function of our BookDataService to access the information. Semantically, nothing is changed in our BookListCtrl controller.

Listing 3.40: Calling getBooks in the BookListCtrl controller

bmApp.controller('BookListCtrl', function ( 
        $scope, $filter, BookDataService) { 
    $scope.getBookOrder = function(book) { 
        return book.title; 
    };

    $scope.books = BookDataService.getBooks(); 

    // This is just to demonstrate the programmatic 
    // usage of a filter 
    var orderBy = $filter('orderBy'),

    var titles = $scope.books.map(function(book) { 
       return {title: book.title}; 
    }); 

    console.log('titles before ordering', titles); 

    // This is the actual invocation of the filter 
    titles = orderBy(titles, 'title'),

    console.log('titles after ordering', titles); 
});

While we are editing the BookListCtrl controller, we can also remove the unnecessary artifacts for programmatic invocation of filters. After all, they add zero value to our application.

The result is shown in Listing 3.41. Since our BookListCtrl controller looks much better now, we can take care of the BookDetailsCtrl controller.

Listing 3.41: The BookListCtrl controller after some clean-up

bmApp.controller('BookListCtrl',
function ($scope, BookDataService) {
    $scope.getBookOrder = function(book) {
        return book.title;
    }; 

    $scope.books = BookDataService.getBooks(); 
});

The $routeParams Service: Selecting the URL parameter

An important aspect for fulfilling our user story is reading the ISBN passed in the URL path parameter. We need the ISBN to get the details of the right book using BookDataService. We implement the getBookByIsbn() function in BookDataService.

Listing 3.42: Reading a URL path parameter in the BookDetailsCtrl controller using the $routeParams service

bmApp.controller('BookDetailsCtrl', 
function ($scope, $location, $routeParams, BookDataService) { 
    var isbn = $routeParams.isbn; 

    [...] 
});

As apparent in Listing 3.42, we use the AngularJS $routeParams service to read the URL path parameter. As usual, we let dependency injection pass it by specifying it as a parameter in the controller’s anonymous constructor. The $routeParams service contains the values of all URL path parameters that we have set for our route configuration. As you can see in the following code snippet, we only have the ISBN (:isbn) passed as the path parameter in the corresponding route.

$routeProvider.when('/books/:isbn', { 
templateUrl: 'templates/book_details.html', 
controller: 'BookDetailsCtrl'
})

We can access a value by accessing the matching property of the $routeParams service. In this case, the access looks like this:

var isbn = $routeParams.isbn;

We store the ISBN in the local variable isbn. We can now use this variable to invoke the getBookByIsbn() function of BookDataService and obtain the Book object whose Details view is called. Before we can do that, we have to get BookDataService by dependency injection. The resulting version of the BookDetailsCtrl controller is given in Listing 3.43.

Listing 3.43: Calling the getBookByIsbn() function in the BookDetailsCtrl controller

bmApp.controller('BookDetailsCtrl', 
function ($scope, $location, $routeParams, BookDataService) { 
    var isbn = $routeParams.isbn; 
    $scope.book = BookDataService.getBookByIsbn(isbn); 

    $scope.goToListView = function() { 
        $location.path(’/books’); 
    }; 
});

After we expanded BookDetailsCtrl to use BookDataService, we can run our application in a browser and see if we can now get every book’s details. To do this we call the URL to our List view.

http://localhost:8080/#/books

Something seems wrong, because there is only one entry in the List view. If we examine the browser console closer, we notice that there is an error message.

Error: Unknown provider: BookDataServiceProvider <- BookDataService

This error looks familiar. We have seen the same message when running our unit tests before we created BookDataService.

The AngularJS injector was not able to find our BookDataService so it could not resolve the dependency in the BookListCtrl controller. This is logical because we have not imported the book_data.js file in our index.html.

Listing 3.44: Including BookDataService in index.html

<!DOCTYPE html> 
<html ng-app="bmApp"> 
<head> 
    [...] 
</head> 
<body> 
    [...] 

    <!-- Scripts --> 
    <script src="lib/angular/angular.js"></script> 
    <script src="lib/angular-route/angular-route.js"></script> 
    <script src="scripts/app.js"></script> 
    <script src="scripts/controllers/book_details.js"></script>
    <script src="scripts/controllers/book_list.js"></script> 
    <script src="scripts/services/book_data.js"></script> 
</body> 
</html>

After we imported the book_data.js file (See Listing 3.44), our BookMonkey application should run correctly and our List view should contain the correct entries.

In addition, we can now obtain each book’s details by clicking on the corresponding entry. Figure 3.18 shows the Details view for the “CoffeeScript” book.

Figure 3.18: The Details view for the CoffeeScript book

We can also tell if our application is functioning correctly by running our E2E testing. These tests should confirm that the introduction of BookDataService has not adversely affected our application.

Summary

  • In addition to the ngScenario DSL for creating E2E tests, AngularJS also comes with the ngMock module, which you can use to include AngularJS modules and allow your application components to be passed through dependency injection and allow you to write unit tests for the components.
  • To create unit tests we chose Jasmine. Other framework we could have used included Mocha and QUnit.
  • The main functions of ngMock are module() and inject().
  • We implemented our BookDataService using a service factory (factory()).
  • BookDataService has a clear public API and an initial rudimentary implementation.
  • We can pass BookDataService by dependency injection to other components. We currently use it in the BookListCtrl and BookDetailsCtrl controllers.
  • We can read a URL path parameter using the $routeParams service.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.212.160