Chapter 4

Extending the Application

In the previous chapter, we implemented basic functions to display book data. In this chapter we will address book creation, editing and deletion and build a small administration area to perform these actions. In addition, in the second part of this chapter we will work on tags for categorizing books by designing special directives.

Finally, to allow more clients to use BookMonkey, we will connect our application to a REST web service so that we can perform the administration of the book data centrally on the server.

The Administration Area

In this section we will build the administration area. In particular, we will write functions to address three aspects. We will start by implementing the remaining CRUD operations for creating, editing and deleting books in our BookDataService. We will ultimately need these functions to implement the corresponding features in the administration area.

After we extend the service, we will take a look at how easily we can process and validate input in an AngularJS form. The FormController and NgModelController controllers are part of the AngularJS core components.

Finally, we will familiarize ourselves with a few new directives that will allow us to reuse the existing templates in other views.

For these features we will write the following user story.

First, the Test

In the first step, we extend our BookDataService to include the remaining CRUD operations for creating, editing and deleting a book. As usual, we specify the required behavior of the API functions with the aid of unit tests. This way, we extend the test suite for the BookDataService with the test cases in Listing 4.1.

Listing 4.1: Unit tests for the BookDataService’s public API

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

    // load the application module
    [...]

    // get a reference to the service
    [...]

    describe('Public API', function() {
        [...]

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

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

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

Listing 4.1 defines unit tests for the remaining functions. We specify that the create function should be called storeBook, the update function updateBook and the delete function deleteBookByIsbn.

There is more to the unit test for the storeBook function. As usual, we simultaneously create a separate suite for the new functions (See Listing 4.2).

Listing 4.2: The unit test for storeBook

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

    [...]

    describe('Public API usage', function() {
        [...]

        describe('storeBook()', function() {
            it('should properly store the passed book object',
            function() {
                var beforeCount = BookDataService.getBooks().length;

                // store an example book
                var book = storeExampleBook();

                expect(BookDataService.getBooks().length).toBe(
                        beforeCount + 1);
                expect(BookDataService.getBookByIsbn(
                        book.isbn)).not.toBeNull();
            });
        });

        [...]
    });

    [...]
});

The implementation of the test is very clear. We store the current number of books in the local variable beforeCount. We obtain this information from the length property of the array that getBooks() returns. Subsequently, we create a new book with the help from the function storeExampleBook(). As shown in Listing 4.3, we use the API function storeBook() inside this function, which should be tested. Other test cases will need a sample book as well. As such, we can outsource the logic that creates a Book object to an external function. It should be noted that the storeExampleBook() function returns a sample book.

If the implementation is successful, the number of books has to increase by one. We check this with an expect() function in conjunction with a toBe() matcher. The resulting number should equal beforeCount + 1. In addition, using the API function getBookByIsbn() we make sure that our service really recognizes the newly created book. According to the getBookByIsbn() function specification, this function cannot return null. A toBeNull() matcher is negated if you chain the call to expect() with not.

Listing 4.3: The helper function storeExampleBook()

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

    [...]

    // Helper functions
    var storeExampleBook = function() {
      var isbn = '978-3-86490-127-0',
        book = {
          title : 'Effective JavaScript (German edition)',
          subtitle : '68 Specific Ways to Harness the Power of'
              + ' JavaScript',
          isbn : isbn,
          abstract :'Do you realy want to master JavaScript?',
          numPages : 240,
          author : 'David Herman',
          publisher : {
              name: 'dpunkt.verlag',
              url : 'http://dpunkt.de/'
          }
      };

      BookDataService.storeBook(book);

      return book;
    };
});

In principle, the test case for the API function updateBook() follows the same pattern (See Listing 4.4). Next, we create our example book by calling storeExampleBook(). Subsequently, we change the example book’s short description (book.abstract) by setting it to “TEST”. We update the changed object by calling the API function updateBook() that we are testing. Consequently, BookDataService‘s getBookByIsbn() should now return the object with an updated short description. Finally, we can write the test case for deleting a book.

Listing 4.4: The unit test for the API function updateBook in BookDataService

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

    [...]

    describe('Public API usage', function() {
        [...]

        describe('updateBook()', function() {
            it('should properly update the book object',
            function() {
                // store an example book
                var book = storeExampleBook();

                // change it
                book.abstract = 'TEST';

                // update the example book
                BookDataService.updateBook(book);

                expect(BookDataService.getBookByIsbn(
                book.isbn).abstract).toBe(book.abstract);
            });
        });

        [...]
    });

    [...]
});

As you can see in Listing 4.5, the unit test for the API function deleteBookByIsbn() is clearer than the previous two test cases. Here we use storeExampleBook() to provide an example. We will immediately delete this book when we call the deleteBookByIsbn() function that we are testing. With the correct result, the getBookByIsbn() function should return null.

Listing 4.5: The unit test for the API function deleteBookByIsbn() in BookDataService

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

    [...]

    describe('Public API usage', function() {
        [...]

        describe('deleteBookByIsbn()', function() {
            it('should properly delete the book object with
              the passed isbn', function() {
                // store an example book
                var book = storeExampleBook();

                // delete the example book
                BookDataService.deleteBookByIsbn(book.isbn);
                expect(BookDataService.getBookByIsbn(
                        book.isbn)).toBeNull();
            });
        });
    });

    [...]

});

At this point, as always, we will execute our unit tests once to ensure that the new test cases fail. If we have not made a mistake, all the newly created test cases will fail. Now we can take care of the implementation of the remaining API functions.

The CRUD Operations of BookDataService

Since we have specified the behaviors of the new API functions storeBook(), updateBook() and deleteBookByIsbn() by using some unit tests, we can continue with the implementation.

First we will discuss the storeBook() function. Along those lines, we will expand on the API object returned by the specified function and delegate the call to our internal implementation function srv.storeBook(). Because until now our BookDataService is still not able to communicate with a backend system, the implementation of the srv.storeBook() function consists of a “one-liner,” with which we insert the new Book object into the internal array srv._books (See Listing 4.6).

Listing 4.6: The implementation of the API function storeBook() in BookDataService

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

    srv._books = [
        ...
    ];

    // Service implementation
    [...]

    srv.storeBook = function(book) {
        srv._books.push(book);
    };

    [...]

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

Basically, we will proceed in the same way with the other two API functions. We will further expand the API object for updateBook() and delegate the call to srv.updateBook(). The implementation of the function srv.updateBook() is therefore somewhat more complex than the implementation of srv.storeBook(). We iterate over our srv._books array in a for loop and look for a Book object whose ISBN matches the passed ISBN. When we find the right object, we call the function angular.extend() from AngularJS to copy the properties of the Book object passed to the function to the Book object found (See Listing 4.7). After calling the extend() function, we let the function terminate early with a return statement. That is a small optimization to prevent us from iterating over more books unnecessarily.

As the name implies, the helper function angular.extend() extends the first object with the properties of the second object. In the case of the same property name, the property of the first object will be overwritten with that of the second object.

Listing 4.7: The implementation of the API function updateBook() in BookDataService

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

    srv._books = [
        ...
    ];

    // Service implementation
    [...]

    srv.updateBook = function(book) {
        for (var i = 0, n = srv._books.length; i < n; i++) {
            if (book.isbn === srv._books[i].isbn) {
                angular.extend(srv._books[i], book);
                return;
            }
        }
    };

    [...]

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

The API function deleteBookByIsbn() is yet to be discussed. We extend our API object and further delegate the call to the internal implementation function srv.deleteBookByIsbn(). Inside this function we once again iterate over all Book objects and search for the book having the same ISBN as the passed ISBN. Using the array function splice(), we separate the Book object found from the array and again prematurely terminate the function to optimize our code.

It should be noted at this point that we iterate over the Book objects using a while loop that decrements an index. This type of iteration is more appropriate in this case, because we need to delete an element from the array during the iteration using the splice function.

Listing 4.8: The implementation of the API function deleteBookByIsbn() in BookDataService

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

    srv._books = [
        ...
    ];

    // Service implementation
    [...]

    srv.deleteBookByIsbn = function(isbn) {
        var i = srv._books.length;
        while (i--) {
            if (isbn === srv._books[i].isbn) {
                srv._books.splice(i, 1);
                return;
            }
        }
    };

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

With this, we have implemented all the CRUD operations in BookDataService. If we have not made a mistake, our new test cases should now pass. This means the operating methods of our service meet the specification. We will incorporate the new API functions when we implement the corresponding features in the administration area.

The ngShow and ngHide directives: Showing and hiding content

After we extended our BookDataService to include create, update and delete functions in the previous section, we are now ready to lay the cornerstone for the administration area.

As the first step, we want to create a special List view for the admin to display an overview of all books. In addition, it should allow a new book to be created and existing books to be deleted (See Figure 4.1). This special List view will become the fundamental view of the administration area, because an admin can use it as a view for creating a new book and editing and deleting a book.

Figure 4.1: The wireframe for the Admin List view

Please note that at this stage we will not go into detail on the corresponding E2E tests for the new views because they do not contain any significant new aspects of the test, and it would only make this chapter longer than necessary. Interested readers can have a look at the corresponding test cases by checking out the project in the accompanying zip file and look in the directory for the current project status.

Listing 4.9: The route configuration for the Admin List view

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

bmApp.config(function ($routeProvider) {

    [...]

    /* Admin routes */
    .when('/admin/books', {
        // we reuse the template from the list view
        templateUrl: 'templates/book_list.html',
        controller: 'AdminBookListCtrl'
    })

    [...]
});

As shown in Listing 4.9, we need a route for the Admin List view. By using when(), we construct the corresponding route configurations in both the /admin/books route in the book_list.html template and the AdminBookListCtrl controller. The comment indicates that we want to reuse the template for the List view. That means we now have two routes that lead to the same template. The only difference is the controller behind the template. For this reuse to be successful, we have to slightly adjust the template for the List view and extend a few aspects. The corresponding extension is shown in Listing 4.10.

Listing 4.10: Extending the template for the List view to make it reusable

<h2 ng-show="isAdmin">Administration Area</h2>
    [...]
        <tr ng-repeat="[...]">
            <td ng-hide="isAdmin">
                <a class="bm-details-link"
                    ng-href="#/books/{{ book.isbn }}"
                    ng-bind="book.title">
                </a>
            </td>

            <td ng-show="isAdmin">
                <a class="bm-edit-link"
                    ng-href="#/admin/books/{{ book.isbn }}/edit"
                    ng-bind="book.title">
                </a>
            </td>
            <td ng-bind="book.author"></td>
            <td ng-bind="book.isbn"></td>
            <td ng-show="isAdmin">
                <a class="bm-delete-link"
                    ng-href="#/admin/books/{{ book.isbn }}/delete">
                    Löschen
                </a>
            </td>
        </tr>
    [...]
<p ng-show="isAdmin">
    <a class="bm-new-book-link" ng-href="#/admin/books/new">
        Create new book
    </a>
</p>

Essentially, we have extended the template so that now several HTML elements can be shown and hidden using ngShow and ngHide directives, respectively. The showing and hiding depend on the scope variable isAdmin. We set this variable to true in the AdminBookListCtrl controller, and we assign no value to it in the BookListCtrl controller. That in effect makes the value false. That means we want to show only those particular HTML elements when our application is displaying the Admin List view. We want to hide these elements, however, in the unrestricted List view.

The <h2> heading “Administration Area,” the hyperlink to edit a book (<a class="bm-edit-link">), the hyperlink to delete a book (<a class="bm-delete-link">) and the hyperlink to create a new book (<a class="bm-new-book-link">) are HTML elements that should be displayed in the admin variant. By contrast, the hyperlink to open the Details view in the Admin List view should be hidden. Therefore, the resulting mutual exclusion that is based on ngShow and ngHide directives means that the book title is linked with the Edit view in the admin variant, whereas in the unrestricted variant it is linked to the book’s Details view.

Now we can continue with the construction of the AdminBookListCtrl controller. For this, we will create the file admin_book_list.js in the app/scripts/controllers/ directory.

Listing 4.11: The AdminBookListCtrl controller

bmApp.controller('AdminBookListCtrl',
function ($scope, BookDataService) {
    $scope.isAdmin = true;

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

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

Listing 4.11 presents the AdminBookListCtrl controller that defines the scope of the Admin List view. Except for the fact that it assigns the value true to the scope variable isAdmin, its implementation of the BookListCtrl controller defines the same scope as that for the unrestricted variant.

That’s everything that needs to be covered about the admin variant of the List view. At this point, we can call the view in the browser. The Admin List view should look like that in Figure 4.2.

Figure 4.2: The Admin List view

Form Processing and Validation with FormController and NgModelController

Most web applications need user input. The HTML standard already defines a form and fields such as the text field, check box and radio button for that purpose. HTML5 extends the repertoire with a variety of other widgets, such as the slider control and input fields for numbers and URLs. Internally, browsers that support HTML 5 validate user input to make sure the user enters valid data.

In this section we will address form processing and validation. Despite the internal validation by the browser, AngularJS brings with it its own mechanism to easily validate form input. The reason for this is that you need a way to influence how validation works. The default mechanisms of HTML5 go in the right direction. However, it is quite limited. For example, with it you cannot format an error message with CSS or display a conditional error message. Thus, many requirements with respect to error handling cannot be implemented. Using the mechanisms that AngularJS provides, you can easily intervene with the validation behavior and easily implement specific requirements.

In terms of form processing, the framework additionally provides a mechanism for integrating user input into the two-way data binding cycle. That means you, for example, can bind specific input fields to scope variables and can thereby perform automatic updates in both directions.

In order to better clarify form processing, we will now use a form to create a new book in our BookMonkey project (See Figure 4.3). You will learn that AngularJS’s validation mechanism is so powerful that you can define any validation for a form completely declaratively in a template, without writing a single line of JavaScript code. In addition, you can expect to use this feature for editing a book using the same template.

In the Admin List view (See Listing 4.10), we have provided a “Create new book” link and connects to the /admin/books/new route. As expected, we have to create this route now. To this end, we once again extend the route configuration with the app.js file (See Listing 4.12).

Figure 4.3: The wireframe of the view for creating a new book

Listing 4.12: The route configuration for the form for creating a new book

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

bmApp.config(function ($routeProvider) {

    [...]

    /* Admin routes */
    [...]

    .when('/admin/books/new', {
        templateUrl: 'templates/admin/book_form.html',
        controller: 'AdminNewBookCtrl'
    })

    [...]

});

As shown in Listing 4.12, the /admin/books/new route, the template in the book_form.html file and the AdminNewBookCtrl controller are cited in the code. That means we still have to create two more files. First, we need a book_form.html file in the /app/templates/admin directory. To do so, we need to create a subdirectory named admin in the templates directory. We will implement the AdminNewBookCtrl controller in an admin_new_book.js file located in the /app/scripts/controllers directory.

Next, we look at the template for creating and editing a book (book_form.html). As shown in Listing 4.13, this template will be designed to be as reusable as the template for the List view. Using the scope variable isEditMode as a switch, we can show or hide some elements. We will eventually set this scope variable to true in the AdminEditBookCtrl controller and set it to false in the AdminNewBookCtrl controller.

As expected, you will find the most interesting aspects of form processing and validation within the <form> element. In AngularJS the <form> element has special semantics. When you use the <form> tag in AngularJS, you don’t just define an HTML form. In AngularJS, the <form> tag represents the form directive. The form directive gets executed and an instance of FormController gets created when a <form> tag is processed. The internal states of this FormController store information about the form, such as whether the form has been altered by the user and whether or not it is valid. You should not implement its logic as it is already covered by the form directive.

The crucial point that makes validation so convenient is the fact that you can set the instance of this controller in the current, valid scope. And you know that you can use a template to output all variables and functions defined within a scope or use them to “feed” the directives. That means you can use the FormController’s internal state properties in a template to specifically show certain content with the ngShow directive.

The questions that spring to mind are: How can you tell AngularJS to pass you the FormController instance in the scope? Which scope variable references the instance? The answer to these questions lie behind the name attribute of the <form> element. The name attribute also has a special meaning within the framework. Using this attribute, you can tell AngularJS to set the instance of the FormController in the scope. The scope variable, through which you can access the instance, is determined by the attribute value. In this case, it is the scope variable bookForm. With that, you can now use the internal properties in the template to conditionally show the specified content. The properties of a FormController include:

  • $pristine – A boolean value that indicates whether or not the user has already interacted with the form.
  • $dirty – Contains the negated value of $pristine.
  • $valid – A boolean value that indicates whether or not the form’s current condition is valid. Its value is determined by each of the respective form components’ validators.
  • $invalid – Contains the negated value of $valid.
  • $error – Refers to an error object that specifies the reason why the form in its current state is invalid.

Listing 4.13: The submit and cancel buttons

<h2>Administration Area</h2>
<div>
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>

        [...]

        <button class="bm-cancel-btn"
                ng-click="cancelAction()">Cancel</button>
        <button class="bm-submit-btn"
                ng-click="submitAction()"
                ng-disabled="bookForm.$invalid">{{ submitBtnLabel }}
        </button>
    </form>
</div>

As shown in Listing 4.13, we use the FormController’s $invalid property to disable the submit button when the form is invalid. The actual deactivation happens when the ngDisabled directive, which depends on the passed scope variable, either activates or deactivates the form component. As we have told AngularJS to provide us with the instance of FormController in the scope variable bookForm, we can use the expression bookForm.$invalid to find out whether or not the form is valid.

We may now ask this question: Based on which FormController properties is a form considered valid or invalid? In the descriptions of the $invalid and $error properties above, we mention that the individual validators of the respective form components are the deciding factor. Listing 4.14 shows how you can provide a form component with a validator. The required attribute used in the <input> field for the book title is responsible for that. Those familiar with HTML 5 know that you can achieve this with the required validator, which tells the browser that the user must enter a value in the corresponding input field. If you use the novalidate attribute in the <form> tag to turn off the default browser validation, the browser will ignore the required validator and other validators in that form. However, the AngularJS required directive gets executed at this point and then, if the necessary precautions have been taken, it activates the corresponding validation for the input field.

Note though that the prerequisite for using the required validator in AngularJS is that the ngModel directive must also be used in the annotated <input> tag. With the help from the ngModel directive, you produce two-way data binding between a form component (e.g., input field) and a scope variable. That means the framework automatically ensures that each input is bound to the scope variable and it also updates the input every time the variable is manipulated.

The critical aspect at this stage is that AngularJS instantiates the ngModel controller with each use of the ngModel directive for the specified form component. The ngModelController, like the FormController, includes data-binding logic as well as the necessary mechanisms to perform the validation of the specified form components. Validator directives like required use this mechanism to feed the necessary validation logic. Therefore, it is imperative that all available validators be used in conjunction with the ngModel directive.

Just as the FormController make the five properties ($pristine, $dirty, $valid, $invalid, and $error) available to assess the overall state of a form, the NgModelController includes these five properties in order to obtain this information for a single form component. That means you can display, for example, error messages that depend on the validation state of a single form component. Listing 4.14 shows that you can display a specific error message for the “title” field. However, here too we pose the question again as to how we can access the five properties of the NgModelController in the template.

Listing 4.14: The form template for creating and editing a book with the required validator

<h2>Administration Area</h2>
<div>
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>
        <input type="text" placeholder="Title..."
            ng-model="book.title" name="title" required>
        <span ng-show="bookForm.title.$dirty
                && bookForm.title.$invalid">
            Please enter a book title.
        </span>
        <br>

        [...]

    </form>
</div>

As with the FormController, the answer for the ngModelController also lies in the name attribute. There is a slight difference with the NgModelController, however. While AngularJS provides you with the FormController instance through the variable specified as a value in the currently valid scope (here, bookForm), the mechanism in the NgModelController works by publishing the instance as a property within the parent FormController. Again, you can specify the name of the property using the attribute value (here, title). That means you can access the NgModelController instance for the “title” field with the expression bookForm.title. Thus, you can use, say, bookForm.title.$dirty or bookForm.title.$invalid. Using both these properties, you can display an error message conditionally. For that, once again you use the ngShow directive. The command in this case would be: If the user has interacted with the “title” field (bookForm.title.$dirty) and one of the validators returned states that the input is considered invalid (bookForm.title.$invalid), then display the appropriate error message.

If you look at Listing 4.15, you can see that the input field for ISBN has been annotated with the required validator. In this case, we tell AngularJS to publish the NgModelController instance within the FormController instance. As such, you can display a conditional error message with the expression bookForm.isbn.$dirty && bookForm.isbn.$invalid.

Listing 4.15: Using the ngDisabled directive

<h2>Administration Area</h2>
<div>
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>

        [...]

        <input type="text" placeholder="ISBN..."
                ng-model="book.isbn" name="isbn"
                ng-disabled="isEditMode" required>
        <span ng-show="bookForm.isbn.$dirty 
                && bookForm.isbn.$invalid">
            Please enter an ISBN.
        </span>
        <br>

        [...]

    </form>
</div>

In addition, we use the ngDisabled directive in Listing 4.15 to make the ISBN uneditable in edit mode.

In addition to the required directive, AngularJS internally includes more validator directives that are intentionally named after the native validators of the corresponding HTML5 attributes to make seamless conversion possible. Specifically, there are validator directives for the following attributes:

  • required. Defines an obligatory input.
  • pattern. Checks of the input against a regular expression.
  • min. Sets a minimum value for a numeric value.
  • max. Sets a maximum value for a numeric value
  • minlength. Sets a minimum length for input.
  • maxlength. Sets a maximum length for input.

In addition, the framework also provides validator directives for HTML5 new input fields.

  • <input type="number"/>. Checks for numeric value.
  • <input type="url"/>. Checks if the input specifies a valid URL
  • <input type="email"/>. Checks if the input specifies an email address

Listing 4.16 shows a partial template that defines the numPages input field. From this you know that you are using two extra validators in addition to the required validator. The first is the validator brought by <input type="number"/>, the other is the min validator, which specifies a minimum value for the input field. That means the result of the expression bookForm.numPages.$invalid depends on these three validators.

Listing 4.16: The min validator in the input fields for numbers

<h2>Administration Area</h2>
<div>
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>

        [...]

       <input type="number" min="1" placeholder="Number of Pages..."
                ng-model="book.numPages" name="numPages" required>
        <span ng-show="bookForm.numPages.$dirty
                && bookForm.numPages.$invalid">
            <span ng-show="bookForm.numPages.$error.min">
                The book must have a minimum number of pages.
            </span>
            <span ng-hide="bookForm.numPages.$error.min">
                Please enter a valid number of pages.
            </span>
        </span>
        <br>

        [...]

    </form>
</div>

We want only one aggregate error message displayed when the required or number validator failed. In addition, if the min validator failed, we want to display instead a special error message that instructs the user to enter a higher number of pages. For that, we use the $error property of the NgModelController.

As just mentioned, the $error property references an error object whose properties are the string representations of the validators that determine that the current input is invalid. In the case of the NgModelController, the values of these properties are certainly not arrays of form components, but rather boolean values that each indicates whether the corresponding validator thinks the input is valid or invalid. Therefore, you can show or hide your special error message using the expression bookForm.numPages.$error.min. Here, you are using the well-known pattern of mutual exclusion as when you employed the ngShow and ngHide directives.

Listing 4.17: The URL validator in URL input fields

<h2>Administration Area</h2>
<div>
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>

        [...]

        <input type="url" placeholder="Publisher's website..."
                ng-model="book.publisher.url" name="publisherUrl"
                required>
        <span ng-show="bookForm.publisherUrl.$dirty
                && bookForm.publisherUrl.$invalid">
            <span ng-show="bookForm.publisherUrl.$error.url">
                Please enter a valid URL.
            </span>
            <span ng-hide="bookForm.publisherUrl.$error.url">
                Please enter the publisher's website.
            </span>
        </span>
        <br>

        [...]

    </form>
</div>

Now, take a look at the code in Listing 4.17. The input field looks the same as the one in Listing 4.16, except we input a URL instead of the number of pages. In this case, we certainly want to display a special error message that depends on the url validator.

The only thing missing in order to complete the feature for creating a book is an AdminNewBookCtrl controller that we will be writing in the admin_new_book.js file. In Listing 4.18 you can see that we are using dependency injection to pass the BookDataService. We will be using this service in the submitAction() function to save the new Book object when the user clicks on the “Create new book” button. In addition, in this function we call the goToAdminListView() function to go from the save operation back to the Admin List view. We use the already known $location service in the goToAdminListView() function.

Listing 4.18: The AdminNewBookCtrl controller

bmApp.controller('AdminNewBookCtrl',
function ($scope, $location, BookDataService) {
    $scope.book = {};
    $scope.submitBtnLabel = 'Create book';

    $scope.submitAction = function() {
        BookDataService.storeBook($scope.book);
        goToAdminListView();
    };

    $scope.cancelAction = function() {
        goToAdminListView();
    };

    var goToAdminListView = function() {
        $location.path('/admin/books'),
    };
});

The important reason that we can implement the submitAction() function with so little source code is the fact that we use two-way data binding between the Book object properties and the form definition in the ngModel directives. That causes the Book object to continue to be populated with the necessary information while the user fills out the form. Finally, all the book information is found in the Book object, and thus the object can be passed directly to the storeBook() function of BookDataService. Actually, we still don’t have to initialize the Book object in the scope ($scope.book = {}). AngularJS will take care of this as soon as the user interacts with the first form component. Certainly there are many third-party directives that cannot handle uninitialized objects. In that case, you should do the initializing yourself.

If we have not made an error up to this point, then it should be possible to create a new book. The form should look like that in Figure 4.4.

Figure 4.4: The form for creating a new book

When you interact with the form, you will learn that even validators we defined are taking effect, and thus the error messages received are displayed in the invalid state.

Binding Templates with the ngInclude Directive

After constructing the form for creating and editing a book, we now want to address the preview. When entering book information, the user will be receiving immediate feedback, as shown in Figure 4.5.

Figure 4.5: The view for adding a new book with preview

Using two-way data binding, you can easily implement a preview, by which you establish data binding with the ngModel directive that will be displayed on the template at some point.

At this point, we can certainly save a lot of template coding if we reuse the template for the Details view. The important element for this type of reuse is the ngInclude directive.

Listing 4.19: Using the ngInclude directive

<h2>Administration Area</h2>
<div class="split-screen">
    <h3 ng-hide="isEditMode">Create new book</h3>
    <h3 ng-show="isEditMode">Edit book</h3>

    <form name="bookForm" novalidate>

        [...]

    </form>
</div>
<div class="split-screen">
    <h3>Preview</h3>
    <div class="simple-border padded"
        ng-include="'templates/book_details.html'">
    </div>
</div>

As you can see in Listing 4.19, you can include a template in another template by using the ngInclude directive. To do this, you have to provide the relative path to the template. Note that the ngInclude directive expects the relative path to be provided by a scope variable. Therefore, static paths must be entered with single quotation marks.

If we have not made any errors at this step, the browser should look similar to that in Figure 4.6.

Figure 4.6: The view with the form for creating a new book that includes live preview

When entering data in the form, you should notice that what you enter immediately appears at the right in the preview. To implement this feature, we have made sure that all object properties (title, subtitle, etc.) have the same names as the input fields.

The Function for Editing A Book

Our BookMonkey application should allow the admin to edit an existing book. We have already added in the Admin List view the appropriate hyperlink for that, which navigates to the Edit Book form. Therefore, we once again have to update our route configuration in app.js and enter the route /admin/books/:isbn/edit (See Listing 4.20). When we open this route in our application, the template from the book_form.html file as well as the AdminEditBookCtrl controller should load. That means we are reusing the template for the Create New Book form in the edit function. You should already be familiar with the necessary changes that need to be done in the template.

Listing 4.20: The route configuration for the Edit Book form

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

bmApp.config(function ($routeProvider) {

    [...]

    /* Admin routes */
    [...]

    .when('/admin/books/:isbn/edit', {
        templateUrl: 'templates/admin/book_form.html',
        controller: 'AdminEditBookCtrl'
     })

    [...]

});

For the AdminEditBookCtrl controller we still have to save the admin_edit_book.js file in the /app/scripts/controllers/ directory. You can see the implementation in Listing 4.21.

Listing 4.21: The AdminEditBookCtrl controller

bmApp.controller('AdminEditBookCtrl',
function ($scope, $routeParams, $location, BookDataService) {
    $scope.isEditMode = true;
    $scope.submitBtnLabel = 'Buch editieren';

    var isbn = $routeParams.isbn;
    $scope.book = BookDataService.getBookByIsbn(isbn);
    $scope.submitAction = function() {
        BookDataService.updateBook($scope.book);
        goToAdminListView();
    };

    $scope.cancelAction = function() {
        goToAdminListView();
    };

    var goToAdminListView = function() {
        $location.path('/admin/books'),
    };
});

For the most part, the AdminEditBookCtrl controller is designed using the same pattern as the AdminNewBookCtrl controller. One of the main differences between the two controllers is the definition of the scope variable isEditMode. We change the value of this variable to show or hide the necessary HTML elements in the template and to disable the ISBN field. Furthermore, we assign the scope variable submitBtnLabel the string “Edit book” to be displayed on the button that will call the submitAction() function when it is clicked. We should also know which book will be edited. For that, we once again access the ISBN, which is presented as a URL path parameter, with the $routeParams service. Using the ISBN, we can get the corresponding Book object with BookDataService.getBookByIsbn() and reference it with the scope variable book. Finally, we update the Book object using BookDataService.updateBook() by calling the submitAction() function. That’s all we need at this point to implement the edit feature. The result is shown in Figure 4.7.

Figure 4.7: The Edit Book form with live preview

The Function for Deleting A Book

Similar to creating and editing a book, deletion is naturally an important function for an admin. The actual view is very simple because it consists of a question and two buttons, one for confirming and one for canceling the operation (See Figure 4.8).

Figure 4.8: The wireframe of the view for deleting a book

We have already added a hyperlink for the delete function in the Admin List view. We just have to edit app.js to update the route configuration, so that when the route /admin/books/:isbn/delete is called, the book_delete.html template and the AdminDeleteBookCtrl controller will be loaded (See Listing 4.22).

Listing 4.22: The route configuration for the Delete Book dialog

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

bmApp.config(function ($routeProvider) {
    [...]

    /* Admin routes */
    [...]

    .when('/admin/books/:isbn/delete', {
        templateUrl: 'templates/admin/book_delete.html',
        controller: 'AdminDeleteBookCtrl'
    })

    [...]
});

As before, we have to create files for it. For the template, we create a book_delete.html file in the /app/templates/admin/ directory. The content of the file is shown in Listing 4.23.

Listing 4.23: The template for the Delete Book dialog

<h2>Administration Area</h2>
<h3>
    Delete the book
    &quot;{{ book.title }}&quot;
    ?
</h3>
<button class="bm-delete-btn"
        ng-click="deleteBook(book.isbn)">Delete</button>
<button class="bm-cancel-btn"
        ng-click="cancel()">Cancel</button>

We only use functions in the template that we have already discussed. We ask the admin whether the book should really be deleted and let him or her confirm or cancel the operation. For both actions we have defined a button. If the admin confirms the deletion, we call the deleteBook() function and pass the ISBN. Otherwise, if the admin cancels the operation, the cancel() function is called.

The AdminDeleteBookCtrl controller also consists of a minimum number of lines of code. We create an admin_delete_book.js file for the controller in the /app/scripts/controllers/ directory. As with all other Java script files we use, we must also include admin_delete_book.js in our index.html using a <script> tag.

Listing 4.24: The AdminDeleteBookCtrl controller

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

    $scope.deleteBook = function(isbn) {
        BookDataService.deleteBookByIsbn(isbn);
        goToAdminListView();
    };

    $scope.cancel = function() {
        goToAdminListView();
    };

    var goToAdminListView = function() {
        $location.path('/admin/books'),
    };
});

Listing 4.24 shows the implementation of the AdminDeleteBookCtrl controller. Like the AdminEditBookCtrl controller, we obtain the ISBN of the book to be deleted with the $routeParams service. We then pass the ISBN to the service function BookDataService.getBookByIsbn() and assign the returned Book object to the scope variable book, so we can access the book title and ISBN in the template. Finally, we define the two functions deleteBook() and cancel() that will be called when the user clicks one of the buttons. The deleteBook() function internally calls the BookDataService.deleteBookByIsbn() function, which actually deletes the book when the admin has confirmed the deletion. Subsequently, we will be forwarded to the Admin List view using the $location service. The forwarding also occurs in the event the user cancels the delete operation.

Figure 4.9 shows how the Delete Book dialog looks like. To view it, you have to click on the Delete hyperlink in the Admin List view. Clicking on “Delete” deletes the book and takes you to the Admin List view. Clicking on “Cancel” takes you back to the Admin List view, though in this case, no book has been deleted.

Figure 4.9: The Delete Book dialog

Summary

  • You can use the ngShow and ngHide directive to either show or hide the content of a template.
  • AngularJS includes a validation mechanism.
  • For every HTML form AngularJS instantiates, you will immediately find the overall state of the form and a FormController in five object properties ($pristine, $dirty, $valid, $invalid, and $error).
  • The overall state is an aggregate of the individual state of the relevant NgModelController instance.
  • For every form component, an NgModelController will be created with the help of the five previously discussed properties, depending on the states of the form components.
  • Validators play an important role in validation. AngularJS includes the corresponding validators for all HTML5 attributes and form components.
  • You can use the ngInclude directive to bind one template with another and thereby making the template easy to reuse.

Creating Categories with Tags

In the previous section, we set up a basic administration area in which we can create, edit and delete a book. Now we want to introduce a way to separate our books into categories. Many modern web applications use tags to annotate a book, and a tag may represent a category to which the book belongs (See Figure 4.10). We can also use these tags, for example, to restrict search results or to search for books in a particular category. However, for now we will only create a list of tags for a book.

Figure 4.10: The diagram of the view for creating a book with a tag input option

Tags usually have a particular form. They are mostly lowercase and do not contain a space. Therefore, in our application, typical tags may look like this: javascript, web, browser or realtime.

As usual, we define a user story for this command.

Extending the Data Model with Tags

In order to be able to categorize our books with tags, obviously we need to extend our data model. To that end, open the book_data.js file that contains the implementation of our BookDataService and extend every Book object in the srv.books array with matching tags (See Listing 4.25).

Listing 4.25: Extending our data model with tags

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

    srv._books = [
        {
            title : 'JavaScript for Enterprise Developers',
            [...]
            tags : [
                'javascript', 'enterprise',
                'nodejs', 'web', 'browser'
            ]
        },
        {
            title : 'Node.js & Co.',
            [...]
            tags : [
                'javascript', 'nodejs', 'web',
                'realtime', 'socketio'
            ]
        },
        {
            title : 'CoffeeScript',
            [...]
            tags : [
                'coffeescript', 'web'
            ]
        }
    ];

    [...]
});

As you can see in Listing 4.25, the book JavaScript for Enterprise Developers are decorated with tags javascript, enterprise, nodejs, web and browser. We also assign tags javascript, nodejs, web, realtime and societio to the book Node.js & Co. In addition, we annotate the book CoffeeScript with coffeescript and web.

On top of that, we have to extend the BookDataService with the getTags() function (See Listing 4.26). We will use this function in the tokenfield directive in order to be able to provide an auto-complete feature for the token input field. At this point, we are only writing the implementation of the getTags() function. The defining of the appropriate test cases is left as an exercise for those interested in it.

Listing 4.26: Extending BookDataService with getTags()

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

    [...]

    srv.getTags = function() {
        var obj = {},
            tag;

        for (var i = 0, n = srv._books.length; i < n; i++) {
            for (var j = 0, m = srv._books[i].tags.length;
                    j < m; j++)
            {
                tag = srv._books[i].tags[j];
                if (!obj.hasOwnProperty(tag)) {
                    obj[tag] = true;
                }
            }
        }

        return Object.keys(obj);
    };

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

Essentially, in Listing 4.26 we use a pair of for loops to iterate over all the tags in a book and use them as keys in the obj object. Because each key can only be used once in an object, the object is practically a set. In other words, it does not allow duplicate tags. We then use Object.keys() to return an array that contains all the keys. That is the data structure the tokenfield plugin expects as a source.

First, the Test

The jQuery plugin “Tokenfield for Bootstrap”

When we discussed tags in the previous section, the central focus was on the construction of special directives. At this point, we want to specify a few unit tests for our tokenfield directive.

The tokenfield directive will simplify input tags in the HTML form for creating/editing a book, because it converts an ordinary <input> field into a so-called token input field. This token input field is provided by a jQuery plugin named “Tokenfield for Bootstrap,” which you can download from this website:

http://sliptree.github.io/bootstrap-tokenfield

To take advantage of the features offered by this jQuery plugin, you have to connect it using our tokenfield directive so that it integrates seamlesssly with two-way data binding. The binding of a jQuery plugin in AngularJS is a use that can be useful in many applications. We also want the enormous ecosystem of jQuery to also be useable from inside AngularJS.

Before we can write unit tests for our future tokenfield directive, we first need to clarify how the tokenfield plugin works. As it is customary for a jQuery plugin, the tokenfield plugin extends a jQuery base object with a tokenfield() function. Thereby we can convert every <input> element into a token input field that allows us to select and ultimately call the tokenfield() function using jQuery. At the same time, we can pass a configuration object to this function to set the method of operation for the token input field. A possible call may look like this:

$('input').tokenfield({
  autocomplete: {
    source: ['javascript','web','browser'],
    delay: 100
  },
  showAutocompleteOnFocus: false,
  allowDuplicates: false,
  createTokensOnBlur: true
});

You can set the token source (by using source) for auto-complete with a delay (delay) the completion should work. The showAutocompleteOnFocus property controls whether auto-complete should be showing when the input field is having focus. In addition, you can use the allowDuplicates property to determine whether or not duplicate tokens should be allowed in the input. The most-recently used property, createTokensOnBlur, controls whether or not the input text should be automatically converted into a token when the input field loses focus.

So when you use the tokenfield() function with the fore-mentioned configuration object, the tokenfiled plugin will render an <input> element as the following DOM structure:

<div class="tokenfield form-control">
  <input type="text" tabindex="-1"
          style="position: absolute; left: -10000px;">
  <span role="status"
          aria-live="polite"
          class="ui-helper-hidden-accessible"></span>
  <input type="text" class="token-input ui-autocomplete-input"
          placeholder=""
          id="1385909441234157-tokenfield"
          style="min-width: 60px; width: 431px;"
          autocomplete="off">
</div>

The original <input> element will be hidden thanks to the CSS properties position: absolute and left: -10000px, and instead the plugin will show other HTML elements to establish the token functionality. Especially at this stage, we should mention the second <input> element with the CSS class token-input, namely the input field in which we enter input data before it is finally converted into a token. In addition, it is worth mentioning that the original <input> element is now a child element of the <div> element generated by the plugin.

Further, we should also look more closely at the DOM elements the plugin generates when we have entered some tokens. With the input javascript, web and browser, the following DOM elements are generated:

<div class="tokenfield form-control">
  [...]
  <div class="token" data-value="javascript">
    <span class="token-label" style="max-width: 419px;">
      javascript
    </span>
    <a href="#" class="close" tabindex="-1">×</a>
  </div>

  <div class="token" data-value="web">
    <span class="token-label" style="max-width: 419px;">
      web
    </span>
    <a href="#" class="close" tabindex="-1">×</a>
  </div>

  <div class="token" data-value="browser">
    <span class="token-label" style="max-width: 419px;">
      browser
    </span>
    <a href="#" class="close" tabindex="-1">×</a>
  </div>

  [...]
</div>

As can be seen in the code above, the plugin generates a <div> element with the CSS class token for every token. Within this element is a <span> element that contains the token text. In addition, there is an <a> tag with CSS class close inside the <div> element. This is the hyperlink for extracting the appropriate token.

The unit tests for the tokenfield directive

We can now write our unit tests. We need to create a directory named directives in the /test/unit/ directory. In the directives directory we will create a file named tokenfield.spec.js that will contain the test suite for our tokenfield directive.

Listing 4.27: The beforeEach() blocks of the test suite for the tokenfield directive

describe('Directive: tokenfield', function () {

    var $compile,
        $rootScope,
        element,
        scope;

    var testTags = ['test1', 'test2', 'test3'];

    // load the application module
    beforeEach(module('bmApp'));

    // get a reference to all needed AngularJS components
    beforeEach(inject(function (_$compile_, _$rootScope_) {
        $compile = _$compile_;
        $rootScope = _$rootScope_;
    }));

    // init logic for every test case
    beforeEach(function() {
        scope = $rootScope.$new();

        scope.book = {
            tags: angular.copy(testTags)
        };

        element = $compile('<input tokenfield="book">')(scope);
    });

    [...]
});

As you can see in Listing 4.27, we define three beforeEach() blocks in our test suite. We put our application module bmApp in the first block. The second block assigns the $compile and $rootScope services to class-level variables using dependency injection. We need both components in the third beforeEach() block to implement the actual initialization logic for every test case.

If the AngularJS automatisms do not work during the execution of the unit tests, you have to finish some directive test tasks manually in order to offer the developer full control over the test sequence. This means you need to create a part of the scope hierarchy that your directive needs for smooth operation.

In our case, that means we have to create a new scope for every test case using the scope function $new(). We define in this scope a rudimentary Book object (scope.book) that only has a tags property. Using this property, we reference an array of test tags by copying the testTags array. We have previously defined the test suite with three test tags, test1, test2, and test3, as local variables.

The last line of the third beforeEach() block is the most important part of the directive testing. We use the $compile service to compile the simple template <input tokenfield="book">. This effectively executes the tokenfield directive specified as an attribute. We assign to the variable element the processed template, which in this case only consists of the <input> element affected by the tokenfield directive. We can then use the variable element to access the individual test cases.

We can start the first test case after we have made the necessary preparation using the beforeEach() blocks to easily implement the test cases.

Listing 4.28: The test case for ensuring a proper initialization

describe('Directive: tokenfield', function () {

    [...]

    it('should properly create available tokens on initialization',
    function () {
        var tokens = element.parent().find('div.token'),
        expect(tokens.length).toBe(testTags.length);
        tokens.each(function(index, token) {
            expect(
                angular.element(token).data('value')
            ).toEqual(testTags[index]);
        });
    });
    [...]
});

Listing 4.28 shows the first test case for our forthcoming tokenfield directive. In the first step, we want to test in this test case if an appropriate token would be generated for each of the three tags from the Book object. We know that for each token the tokenfield plugin creates a <div> element that contains other elements.

However, since we only have a reference to the original <input> element that will still be inserted as a child node in the parent <div> element, we have to do something to reference the parent <div> element.

After a successful execution of our directive, the element will contain exactly three <div> elements with a CSS class token. We obtain a jQuery object using the statement find('div.token') that is contained in the found <div> element with the CSS class tokens. We assign this jQuery object to the local variable tokens. Finally, we can use the length property to expect there must be as many <div> elements with the CSS class token as there are tags in the testTags array.

In addition, we can use each() to iterate over the individual div.token elements and expect for each element that the token content must match the corresponding tag in the testTags array. We must be careful in doing that, so that we also obtain the corresponding DOM element within the callback function in addition to the index. To create a jQuery object from the DOM element and to be able to call the data() function, we have to use the angular.element() function.

Our second test case deals with creating new tokens. To do this, we first determine the current number of input tokens using the same call we used before, and then assign it to the local variable tokenCount (See Listing 4.29). In addition, we get a reference to the input.token-input input field and store it in the local variable tokenInput. Recall that it is the input field the user will instantiate and whose content will eventually be converted into tokens.

Listing 4.29: The test case for adding new tokens

describe('Directive: tokenfield', function () {

    [...]

    it('should properly add new tokens', function () {
        var tokenCount = element.parent().find('div.token').length,
            tokenInput = element.parent().find('input.token-input'),
            testToken = 'test4';

        tokenInput.focus();
        tokenInput.val(testToken);
        tokenInput.blur();
        var tokenCountAfter
            = element.parent().find('div.token').length;
        expect(tokenCountAfter).toBe(tokenCount + 1);
        expect(scope.book.tags.length).toBe(tokenCountAfter);
        expect(element.parent().html()).toContain(testToken);
    });
    [...]
});

We set focus on the input.token-input input field (using tokenInput.focus()), set its value to “test4” (tokenInput.val(testToken)), and remove focus from it (tokenInput.blur()). As a result, the tokenfield plugin will create a new token for the given input once the input field loses focus. Accordingly, we develop the expectation that another div.token element should now be present.

Because we will seamlessly connect the tokenfield plugin to AngularJS’s two-way data binding using the tokenfield directive, the array should now contain another element as well. Finally, the last expectation tests whether the returned string actually was converted to a token and not something else.

Listing 4.30: The test case for token removal

describe('Directive: tokenfield', function () {

  [...]

  it('should properly remove new tokens', function () {
    var indexToRemove = 0;

    angular.element(
      element.parent().find('div.token')[indexToRemove]
    ).find('a.close').click();

    expect(
      element.parent().find('div.token').length
    ).toBe(testTags.length - 1);
    expect(
      scope.book.tags.length
    ).toBe(testTags.length - 1);
    expect(
      element.parent().html()
    ).not.toContain(testTags[indexToRemove]);
  });
});

Listing 4.30 presents the last unit test for our forthcoming tokenfield directive. In this test case, we want to ensure the proper removal of existing tokens. To do that, we assign zero to the local variable indexToRemove to indicate that we want to delete the first token. Subsequently, we reference the a.close hyperlink under the div.token element and trigger the click event. As a result, the tokenfield plugin will remove the corresponding token. At the same time, the number of elements in the scope.book.tags array should have been reduced by one, and the string of the deleted token should no longer appear in the tokenfield input field.

Before we run the recently created test suite to make sure that the unit tests for the tokenfield directive actually fail, we should extend the Karma configuration for unit testing the dependencies we will need to implement the tokenfield directive. Therefore, the files property should now have the following value:

// list of files / patterns to load in the browser
files: [
    'app/lib/jquery/jquery-1.10.2.min.js',
    'app/lib/jquery-ui/jquery-ui.min.js',
    'app/lib/bootstrap-tokenfield/bootstrap-tokenfield.min.js',
    'app/lib/angular/angular.js',
    'app/lib/angular-route/angular-route.js',
    'app/lib/angular-mocks/angular-mocks.js',
    'app/scripts/app.js',
    'app/scripts/**/*.js',
    'test/unit/**/*.js'
],

Alternatively, you can copy the new configuration from the corresponding project folder in the accompanying zip file for the book.

Next, you can use the known console command to run the unit test to ensure that the three new test cases fail.

The tokenfield Directive: Creating the Tag

Have presented the jQuery plugin “Tokenfield for Bootstrap” and defined the corresponding test cases in the previous section, we now need to deal with the implementation of the tokenfield directive. To do that, we set two goals with regard to the creation of this directive: First, we want to use a more complex example to show how powerful this AngularJS feature is. Second, we want to use an actual example of a tokenfield plugin to show how a jQuery plugin can be integrated into AngularJS such that the plugin can work seamlessly in two-way data binding.

We will extend the form for creating/editing a book to use the tokenfield directive. This jQuery plugin also provides an auto-complete feature in addition to the token allocation feature. That means the admin has less writing to do to incorporate the tags for a book.

Unfortunately, the tokenfield plugin requires some dependencies. In particular, we now have to include the following third-party libraries in our index.html to be able to use the tokenfield plugin.

  • jQuery (http://jquery.com/)
  • jQuery UI (http://jqueryui.com/)
  • Twitter Bootstrap (only the style sheet at http://getbootstrap.com/)
  • Tokenfield for Bootstrap (http://sliptree.github.io/bootstrap-tokenfield/)

In the accompanying zip file for the book, we have prepared the project along with all of the dependencies so that you don’t have to search for them. In addition, in Chapter 5, “Project Management and Automation” we will explain further how you can easily manage our frontend dependencies with Bower.

Next, we create a subdirectory named directives in the /app/scripts/ directory for our directive. We will create a file named tokenfield.js in the directives directory, which will contain the implementation of our tokenfield directive.

In Chapter 2, “Basic Principles and Concepts” we have used the AngularJS feature for defining a directive. As we can observe in Listing 4.31, we decided on the definition in the tokenfield directive using a directive definition object (DDO).

We specified in our unit tests that we wanted to use the tokenfield directive as an HTML attribute. To do that, we entered “A” (for attribute) in the restrict property of the DDO. Actually, we could also skip this step, because the framework by default assumes that we want to create new HTML attributes with directives.

Furthermore, we have defined the requirement in the unit tests that the directive should contain a Book object as input. That means we have to enter the scope property of the corresponding DDO. When we assign an object to the scope property of the DDO, AngularJS will create its own isolated scope for every instance of our tokenfield directive. Since we need to access the given Book object within the directive, we define two-way data binding to the parent scope by assigning the equal sign (“=”) to the tokenfield property’s object. That means now we can access the given Book object in the link function using scope.tokenfield. With the two-way data binding we defined with the equal sign, all the changes that we make to the Book object within the directive are immediately shown in the parent scope as well. You will learn that this property is crucial in being able to see the given tags directly in the preview.

Listing 4.31: The tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        restrict: 'A',
        scope: {
            // tokenfield holds a two-way bounded
            // reference to the book object
            tokenfield: '='
        },
        link: function(scope, elem) {
            [...]
        }
    }
})

In Chapter 2, “Basic Principles and Concepts” you learned that a directive is characterized by its link function. You define a link function by assigning a function to the DDO’s link property. AngularJS calls the link function just once for each directive instance. That means you can initialize the instance within the link function. Typically, you register specific event handlers in the link function and carry out the DOM manipulations.

In Listing 4.32 we register two event handlers so that we can respond to certain events within the tokenfield plugin. Before we do that, however, we initialize the plugin by calling the tokenfield() function. Since the elem object is a jQuery object that wraps the DOM element on which the directive acts and we have decided in the unit tests that our tokenfield directive is primarily intended to act on <input> elements, we can call this function directly on the elem object.

Thus, we use the same configuration object as the one we used in the previous section when we introduced plugins. However, here we use a different source for the auto-completion. We allow the corresponding source array to receive output from the BookDataService by calling getTags(). This requires that BookDataService be integrated via dependency injection. Specifically, it is important that we set the createTokensOnBlur property to true in the configuration object in order to automatically convert all text in a token when the tokenfield field loses focus. Recall that we defined a unit test we could only satisfy using this property.

Listing 4.32: The tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
  return {
      [...]
      link: function(scope, elem) {
          elem.tokenfield({
              autocomplete: {
                  source: BookDataService.getTags(),
                      delay: 100
                  },
                  showAutocompleteOnFocus: false,
                allowDuplicates: false,
                createTokensOnBlur: true
            }).on('afterCreateToken', function (e) {
                addToken(e.token.value);
            }).on('removeToken', function (e) {
                removeToken(e.token.value);
            });

            [...]
        }
    }
});

As mentioned before, we registered two event handlers after the initialization. First, we want our local function addToken() to be invoked when the event afterCreateToken occurs. Second, the occurrence of the removeToken event should call the local function removeToken(). In both cases we pass the created or removed token that we can access with e.token.value.

Listing 4.33: The tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        [...]
        link: function(scope, elem) {
            [...]

            // only call $apply when directive is initialized   
            // to avoid 'digest already in progress'
            var initialized = false;
  
            function addToken(token) {
                if (initialized) {
                    // $apply() to trigger dirty checking
                    // because of 3rd-party callback
                    scope.$apply(function() {
                        scope.tokenfield.tags.push(token);
                    });
                }
            }
        } 
    }
});

Now we can deal with the implementation of the local functions addToken() and removeToken(). Both functions are responsible for integrating the tokenfield plugin into the two-way data binding cycle.

Let’s now look at the addToken() function in Listing 4.33. This function is actually quite simply designed, because it does nothing more than inserting a new token into the tags array of our Book object (scope.tokenfield). The key point is that we implement this logic in a callback function passed to the scope.$apply() function. This means that immediately after the insertion logic is executed, AngularJS’s dirty checking is triggered, which eventually ensures that the newly entered change is visible in all other components within AngularJS. We have to do it this way, because we perform scope manipulations within a callback function that come from a third-party library. Thus, we must be informed of any changes using scope.$apply(). If we did not do that, the other components within the framework would not register the changes until the next dirty checking cycle.

In addition, we introduced a flag named initialized and would only execute the logic we just discussed if the flag was set to true. You will learn that this flag is needed to get the so-called “Digest already in progress” errors out of the way. AngularJS raises these errors if you try to trigger dirty checking when you are already in a dirty checking cycle. Since the link function is carried out in a dirty-checking cycle, under a certain condition you may get a "Digest already in progress" error when you call scope.$apply() . We will discuss this condition in more detail later

Let’s take another look at the removeToken() function in Listing 4.34.

Listing 4.34: The tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        [...]
        link: function(scope, elem) {
            [...]

            function removeToken(token) {
                if (initialized) {
                    // $apply() to trigger dirty checking
                    // because of 3rd-party callback
                    scope.$apply(function() {
                        var tags = scope.tokenfield.tags,
                            i = tags.length;
                        while(i--) {
                            if (token === tags[i]) {
                                tags.splice(i, 1);
                                break;
                            }
                        }
                    });
                }
            }

            [...]
        }
    }
});

The same rules naturally apply to the removeToken() function as well. However, in this function we manipulate the scope in a callback function called by the plugin and must manually trigger dirty checking with scope.$apply(). The actual logic for removing a token that was returned by the tokenfield plugin is once again very simple. We iterate over the book tags and delete the corresponding tag once we find a match.

Finally, we discuss the init() function that we will immediately call after defining it (See Listing 4.35). At this point, it is also clear how much the flag initialized helps us get rid of the “Digest already in progress” error.

Listing 4.35: The tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        [...]
        link: function(scope, elem) {
            [...]
            function init() {
                if (angular.isDefined(scope.tokenfield.tags)) {
                    if (scope.tokenfield.tags.length > 0) {
                        // this call emits an 'afterCreateToken'  
                        // event and this would imply a 'digest
                        // already in progress' without the 
                        // initialized flag
                        elem.tokenfield('setTokens',
                                         scope.tokenfield.tags);
                    }
                }
                else {
                    scope.tokenfield.tags = [];
                }
 
                initialized = true;
            }

            init();
        }
    }
});

Our tokenfield directive has to deal with two potential call scenarios, when we edit a book and when we create a new book. In the first case, it is very likely that the Book object has already been annotated with the specified tags. This means that, in this case, we must ensure that the tokenfield plugin has been populated with the existing tags so that it can generate the corresponding tokens. The call looks like this:

elem.tokenfield('setTokens', scope.tokenfield.tags);

The effect of this call is why we would get the “Digest already in progress” error, if we had not been using the initialized flag. With this call, the default for the tokenfield plugin also means that the plugin will raise the afterCreateToken event for every token created. Recall that we call the addToken() function in response to this event, which means it internally initiates dirty checking using scope.$apply(). That means we would make the mentioned error by initiating another dirty checking during a dirty checking cycle. If we first set the initialized flag to true after we have initiated the potential default, this error will now be excluded.

Listing 4.36: Using the tokenfield directive in the template with the form for creating/editing a book

<h2>Administration Area</h2>
<div class="split-screen">
    [...]

    <form name="bookForm" novalidate>
        [...]

        <input tokenfield="book">

        [...]
    </form>
</div>
<div class="split-screen">
    <h3>Preview</h3>
    <div class="simple-border padded"
         ng-include="'templates/book_details.html'">
    </div>
</div>

That’s everything at this point for wrapping up the tokenfield plugin in AngularJS. If we now use our tokenfield directive according to our definition in the template with the form (book_form.html) for creating/editing a book (See Listing 4.36), then we should see a token input field (See Figure 4.11) that integrates seamlessly in the two-way data binding cycle. With this input field, we can now easily group our books into categories by giving each one of them appropriate tags. Additionally, the defined unit test should now pass.

The result will be even clearer once we have implemented the directive tags for outputting the tag in the next section.

Figure 4.11: The form for creating/editing a book with the tokenfield directive

The tags Directive: Showing the tags

In addition to an input option for tags, naturally we also have to create a mechanism that can display the tags. As already mentioned, we also want to once again implement a directive that addresses that.

The tags directive should define a new HTML element <tags>. Furthermore, we should be able to specify a data source for the tag output using an HTML attribute (tag-data). We want to specify a string array as the data source. Listing 4.37 shows the template for the book Details view (book_details.html) using our tags directive. We set the tag-data attribute to book.tags to pass the array of book tags to the directive.

Listing 4.37: Using the tags directive in the detailed view

<h2 ng-bind="book.title" class="bm-book-title"></h2>
<h3 ng-bind="book.subtitle" class="bm-book-subtitle"></h3>
<p>
    [...]
    <tags tag-data="book.tags"></tags>
</p>
<hr>
<p ng-bind="book.abstract" class="bm-book-abstract">
</p>
<a ng-click="goToListView()"
  href=""
  class="bm-list-view-btn">
  Zurück
</a>

The implementation of the tags directive is straightforward. Next we will create the tags.js file in the /app/scripts/directives/ directory. The file will contain the directive implementation. Listing 4.38 shows the content of this file.

Listing 4.38 : The tags directive

bmApp.directive('tags', function() {
    return {
        restrict: 'E',
        templateUrl: 'component_templates/directives/tags.html',
        scope: {
            tagData: '='
        }
    }
});

In Listing 4.38 we define the directive using a directive definition object (DDO). We set the restrict property to “E” to notify AngularJS that we want to define an HTML element with this definition. Additionally, we give our directive its own template using the templateUrl property. Also, we assign to the scope property an object containing a tagData property. With this property, we provide our directive with access to the tag-data attribute. All of these comply with the naming conventions discussed in Chapter 2, “Basic Principles and Concepts.” Since we want to maintain a two-way bound reference, we assign the value “=” to the tagData property. You will learn that this two-way binding is crucial to automatically updating our directive’s tag output when the content of the array changes.

Now we still have to define the template. For that, we create a directory named component_templates in the app directory of our application. In this directory we will also create a subdirectory named directives, which will be the location of the tags.html file that we assigned to the templateUrl property in Listing 4.38. The tags directive is shown in Listing 4.39. For the output of the two-way data binding, we use the ngRepeat directive to iterate over the tagData array and display the value of the tag variable we use in the loop. The CSS class bm-tag in the <span> element gives our tags a blue background and rounded corners. We won’t go into detail about how the CSS class works here.

Listing 4.39: The template of the tags directive

<span class="bm-tag" ng-repeat="tag in tagData">
    {{ tag }}
</span>

Since the ngRepeat directive automatically generates a <span> element for every element in the array and also automatically updates the output in the event of a data change in the array, we will now get direct feedback in the preview of the form for creating/editing a book when we populate or remove a tag using the tokenfield directive. Recall that we reuse the template for the Details view in the template for the book creation/editing form to create the live preview.

Figure 4.12: The live preview of the form for creating/editing a book with tags directive

Summary

  • The internal mechanisms of AngularJS in unit tests are not automatically executed in order to give the developer full control over the test procedure.
  • That means that you have to manually handle some aspect of the test when testing directives that the framework would normally execute automatically under normal operation.
  • In particular, this means you have to manually compile the template used in the directive using the $compile service. When compiling, AngularJS will recognize the tested directive and thereby execute its link function.
  • You always compile a template against a scope. You can generate a new scope in unit tests, passing the $rootScope from AngularJS via dependency injection and calling the $new() function on the $rootScope.
  • You should often review the state of the DOM in unit tests for directives using jqLite over angular.element(). That is the only jQuery implementation that AngularJS provides. However, the tradeoff is that jqLite only provides a subset of the functions in jQuery.
  • If you include the correct jQuery implementation before including AngularJS in your index.html, AngularJS will set and delegate every call from angular.element() to the jQuery implementation.
  • In AngularJS it seems simple to use directives to connect to existing jQuery plugins and other third-party libraries.
  • However, you should make sure that you integrate a plugin in the two-way data binding cycle correctly. Therefore, you should ensure that you manually initiate dirty checking using scope.$apply() with every scope manipulation executed within a callback function that is executed within a third-party library.

Connecting to A REST Web Service

To be honest with you, our application is still pretty boring because each user has his or her own data in the browser, and there is no data management. This means it is not possible to synchronize the book data between two active applications. In this section we would like to lift this restriction by managing our application data centrally on a server. Communication with a backend is one of the typical application cases for single-page applications.

As we already mentioned at the beginning of this project that we would be using a backend implementation that is based on Node.js, we will use Express Frameworks (http://expressjs.com) to set up an endpoint that will allow our AngularJS application to send HTTP requests. We have tried as much as possible to design the backend interface “RESTfully.” Thus, our backend is a REST web service.

At this point, we define the command using a user story.

The BookMonkey Backend

At this point, we want to briefly present the backend. The REST endpoint enables calls on both the books and tags resources and uses JSON as its data format. In particular, we allow HTTP requests to these resources:

// books resource
GET /api/books // get all books
POST /api/books // create a new book
GET /api/books/:isbn // get a book by isbn
PUT /api/books/:isbn // update the book with the specified isbn
DELETE /api/books/:isbn // delete the book with the specified isbn

// tags resource
GET /api/tags // get all tags

Thanks to this interface definition we can describe the basic CRUD operations offered by our BookDataService. For example, with a GET /api/books, we get an array of Book objects. On the other hand, POST /api/books/:isbn returns a single book object. With a POST /api/books and a PUT /api/books/:isbn, the backend expects a Book object. The request for deleting a book, DELETE /api/books/:isbn, proceeds without user data. We get all the system tags with GET /api/tags. The success or failure of a request is indicated by an HTTP status code.

That is everything we need to know about the backend to be able to connect our BookMonkey application. However, we should still look briefly at what is left to do to connect to the server.

Next you have to make sure you have installed the current version of Node.js on your machine. That should have happened already when you began designing your BookMonkey project (See Chapter 3). Now you have to install Express Framework for the server. To do that, change directory to the 04_00_backend project directory of the extracted zip file you downloaded from the publisher’s website. Here, you will find a package.json file. Then, type this to download all the dependencies defined in package.json from the npm registry

npm install

Once you have downloaded the dependencies, you can start the server by entering the following command:

node server.js

If the environment variable PORT is not defined, or if it contains an invalid value, the server starts on the default port 4730. Otherwise, the server will connect to the port specified by the environment variable.

You can test the connection by visiting this URL from your browser (assuming the server is running on port 4730):

http://localhost:4730/api/books

You should see a JSON array containing all our Book objects in the response as shown in Figure 4.13.

Figure 4.13: Response from calling GET /api/books

HTTP Communication with the $http Service

In order to reload single-page application data asynchronously using HTTP when the application is running, the browser needs to use the so-called XMLHttpRequest object. The underlying program model is known as AJAX (Asynchronous JavaScript and XML). Using the XMLHttpRequest object directly is not recommended as you still have to write some code and address cross-browser issues. The best way to use AJAX is by using a library like jQuery that provides a wrapper for XMLHttpRequest.

AngularJS also includes a component that greatly simplifies the sending of HTTP requests: the $http service. It is the best you’ve got for working with AJAX as it ensures that after receiving an HTTP response any scope manipulation will trigger dirty checking. The $http service API is based on the so-called promises.

A promise represents the eventually arriving result of an asynchronous operation and is largely responsible for your ability to comfortably work with asynchronous APIs. It is especially responsible for your not losing yourself in the whole “callback bustle” and avoiding the so-called “pyramid of doom.” We will provide the relevant background information in Chapter 7, “Frequently Asked Questions.”

If the $http service only consists of a function, you can specify an HTTP request that will call this $http() function. The function would then expect as a parameter a configuration object that describes how the HTTP request should be constructed. The following properties are among the most important properties of this configuration object:

  • method – specifies the HTTP method as a string (e.g., “GET” or “POST”)
  • url – specifies the absolute or relative URL to be requested
  • data – the user data that should be sent with the request. The service automatically takes care of the deserialization from and serialization to JSON.
  • headers – an object that describe which HTTP headers should be sent.

In addition to the core properties, there are other properties that you can use to configure certain aspects of the HTTP request. You can find the complete overview in the official API documentation of the $http service at http://docs.angularjs.org/api/ng.$http.

In order to call, for example, the books resource of our backend, the corresponding $http() call would look like this:

$http({
  method: 'GET',
  url: 'http://localhost:4730/api/books'
});

The output of an $http call is always a promise. That means that in our complete application we can pass around this promise and register a callback function with all components interested in this HTTP request. We will see later how to do that after we design the BookDataService.

The framework offers the following help functions:

  • $http.get()
  • $http.head()
  • $http.post()
  • $http.put()
  • $http.delete()
  • $http.jsonp()

For example, you can simplify the retrieval of the books resource with the following code:

$http.get('http://localhost:4730/api/books'),

First, the Test

We already mentioned repeatedly that AngularJS has been designed from the ground up with testability in mind. In addition to ngScenario DSL for E2E tests and the ngMock module’s module() and inject() base functions, the provided mock implementation for this property is also particularly important.

In this section we focus on one of these mock objects, namely the $httpBackend mock object. The background is that the $http service uses an $httpBackend object to execute the actual HTTP request. In the unit tests, you usually replace this $httpBackend object with the $httpBackend mock object to avoid sending actual requests to a server and make it possible to define your own responses. In addition, the $httpBackend mock object allows you to formulate expectations that ensure that a component will really execute certain requests when they are executed in production. These are the so-called request expectations. This way, you can easily test components that communicate with a backend.

Our BookDataService is one such component. Therefore, we will now modify the existing test suite in the file book_data.spec.js to specify which HTTP requests we expect and when the API functions of BookDataService will be called. To formulate the request expectations, we will use the $httpBackend mock object. We will not describe all the test cases in detail. Instead, we will pick a test case to show how we can easily set predefined responses with the $httpBackend mock object and formulate request expectations.

Next, we have to inject the $httpBackend mock object using dependency injection. We assign the mock object to the local variable $httpBackend so it can be accessed by all calls to beforeEach() and afterEach() as well as to describe() or it(). We do not have to do anything at this stage to actually get our hands on the mock object because we have included the files angular-mocks.js and angular.js in our Karma configuration for unit tests (karma.conf.js). For the unit tests, the $httpBackend object will be overwritten by the $httpBackend mock object.

Listing 4.40: The beforeEach blocks of the new version of the BookDataService’s test suite

describe('Service: BookDataService', function () {
    // just to match the baseUrl in the service
    var baseUrl = 'http://localhost:4730';

    var BookDataService, $httpBackend;

    // load the application module
    beforeEach(module('bmApp'));

    // get a reference to all used services
    beforeEach(inject(function (_BookDataService_, _$httpBackend_) {
        BookDataService = _BookDataService_;
        $httpBackend = _$httpBackend_;
    }));
    [...]
});

In the last beforeEach() block, we set some predefined HTTP responses for the $httpBackend mock objects (See Listing 4.41). For that, we use the when() function of the $httpBackend mock object. Therefore, if the $http service sends an HTTP GET request to baseUrl + '/api/books' during the execution of our unit tests, the $httpBackend components will respond with our test array of Book objects (testBooks). This test array contains the three books we have often used.

This way, we set a predefined response for every HTTP request submitted during the execution of our unit tests. It is important that we perform this step at least for every HTTP request for which we will formulate a request expectation in the test cases. Otherwise, the test cases that have no predefined response will fail with the message “No response defined!”

Listing 4.41: Definition of the predefined HTTP responses

describe('Service: BookDataService', function () {
    [...]

    // define trained responses
    beforeEach(function() {
    $httpBackend.when(
        'GET', baseUrl + '/api/books'
    ).respond(testBooks);

    $httpBackend.when(
        'GET', baseUrl + '/api/books/' + csBook.isbn
    ).respond(csBook);

    $httpBackend.when(
        'GET', baseUrl + '/api/books/test'
    ).respond(404, ''),

    $httpBackend.when(
        'POST', baseUrl + '/api/books'
    ).respond(true);

    $httpBackend.when(
        'PUT', baseUrl + '/api/books/' + csBook.isbn
    ).respond(true);

    $httpBackend.when(
        'DELETE', baseUrl + '/api/books/' + csBook.isbn
    ).respond(true);
    });

    [...]

});

In Listing 4.42 we use an afterEach() block for the first time. As you know, this block gets executed once after every test case. In this block we use verifyNoOutstandingExpectation() to check after every test case if there are still outstanding request expectations that have not been fulfilled. If that is the case, that means an expected HTTP request sent via the code being tested was not deleted. This would therefore cause the test case to fail.

Next, we use verifyNoOutstandingRequest() to test if there are still existing HTTP requests. This way, we check the opposite case. Are there still existing HTTP requests? If that is the case, then additional requests were submitted after the requests for which we had formulated an expectation. We don’t generally allow that, so we also build a verifyNoOutstandingRequest() test after every test case.

Listing 4.42: The afterEach() block of BookDataService’s test suite

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

[...]

    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });

[...]

});

Now we can finally show you a test case, which we print in Listing 4.43. This is a test case for saving a new book. Other test cases will follow the same pattern.

Listing 4.43: The test case for saving a new book

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

    [...]

    describe('Public API usage', function() {
        [...]

        describe('storeBook()', function() {
            it('should properly store the passed book object',
            function() {
                $httpBackend.expectPOST(
                    baseUrl + '/api/books', effectiveJsBook
                );
                BookDataService.storeBook(effectiveJsBook);
                $httpBackend.flush();
            });
         });

        [...]
    });

    [...]

    // Helper objects
    var effectiveJsBook = {
        title : 'Effective JavaScript (German Edition)',
        [...]
    };

});

The critical point when formulating a request expectation is calling the expect() function. As you can see in Listing 4.43, for every HTTP method (GET, POST, etc.) there is a corresponding help function that makes it easier for you to formulate a request expectation.

Therefore, in this case we expect that the function BookDataService.storeBook() internally uses the $http service to submit an HTTP POST request to the URL baseUrl + '/api/books'. In addition, we expect that with this request the user data that is defined in the local variable effectiveJsBook will be sent along.

We should not forget to call $httpBackend.flush() at the end of the function. Otherwise, the verifyNoOutstandingRequest() test in the afterEach() block would fail. The reason for this is that the actual $httpBackend object works asynchronously. Therefore, the $httpBackend mock object must also adhere to these rules to keep it from altering the code execution. Testing asynchronous code is not that simple and requires some extended Jasmine constructs like runs() and waitsFor(). Using flush(), we can make sure that our tests, despite being asynchronous, can be executed synchronously in the unit test. With this call we can set the point in the test where the corresponding HTTP response will be returned and thereby simulate a response from the server.

With the $httpBackend mock object you can test your AngularJS application with ease.

We leave the adaptation of the remaining test case as an exercise for you. Alternatively, you can open the new version of the file book_data.spec.js from the zip file accompanying this book.

Using $http in BookDataService

In the previous section, we adapted the unit tests for the BookDataService so that it would send HTTP requests to our backend when its API functions were called. Now we want to update the new implementation to fulfill these specifications.

We will therefore ensure that, at this point, we have to make an API break. It will become clear shortly why we have to do that.

As shown in Listing 4.44, we let the $http service pass via dependency injection so that we can send HTTP requests using this service. In addition, we have to adapt the returned API object a bit. In the new version, all the API functions essentially return something. This is also why our API breaks compared to the previous version. Previously the API calls directly returned the data available locally. By contrast, now every access is associated with an HTTP request and this process occurs asynchronously, so we handle it in the most elegant way by simply returning the promise that provides us with the corresponding $http call. Thus, a BookDataService caller (e.g., all controllers) can decide what should happen when the HTTP response arrives from the server. More importantly, the caller can decide what should happen if it receives an error in the response.

Listing 4.44: BookDataService’s new API object

bmApp.factory('BookDataService', function($http) {
    [...]

    // Public API
    return {
        getBookByIsbn: function(isbn) {
            return srv.getBookByIsbn(isbn);
        },
        getBooks: function() {
            return srv.getBooks();
        },
        getTags: function() {
            return srv.getTags();
        },
        storeBook: function(book) {
            return srv.storeBook(book);
        },
        updateBook: function(book) {
            return srv.updateBook(book);
        },
        deleteBookByIsbn: function(isbn) {
            return srv.deleteBookByIsbn(isbn);
        }
    };
})

Listing 4.45 shows the implementations of the read operations. As expected, the implementations break, because we simply send a request to the server using $http and return the resulting promise. This way, we direct all API calls to the corresponding resources that are available to us in our REST web service.

Listing 4.45: Read accesses are converted using HTTP GET requests

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

    srv._baseUrl = 'http://localhost:4730';

    // Service implementation
    srv.getBookByIsbn = function(isbn) {
        return $http.get(
            srv._baseUrl + '/api/books/' + isbn
        );
    };

    srv.getBooks = function() {
         return $http.get(
             srv._baseUrl + '/api/books'
         );
    };

    srv.getTags = function() {
        return $http.get(
            srv._baseUrl + '/api/tags'
        );
    };

    [...]
});

What about the write operations? Naturally we also proceed with the write operations as shown in Listing 4.46. Here as well, an API call will simply be directed to the relevant REST endpoint. We return the resulting promise as before so that the caller can decide how to handle the HTTP response.

Listing 4.46: The write operations are handled using HTTP POST/PUT/DELETE requests

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

    srv._baseUrl = 'http://localhost:4730';

    // Service implementation
    srv.storeBook = function(book) {
        return $http.post(
            srv._baseUrl + '/api/books', book
        );
    };

    srv.updateBook = function(book) {
        return $http.put(
            srv._baseUrl + '/api/books/' + book.isbn, book
        );
    };

    srv.deleteBookByIsbn = function(isbn) {
        return $http.delete(
            srv._baseUrl + '/api/books/' + isbn
        );
    };

    [...]
});

After the revision, our BookDataService looks pretty slim, because we only design the API functions in the relevant HTTP requests. However, these changes have far-reaching effects on our application’s functionality.

When we execute our unit tests, the modified test cases for the BookDataService should turn “green” now. However, the test cases for our tokenfield directive will fail because the directive–like all other users of BookDataService--does not expect that our service now returns promises

With this change we really shake up a large part of our E2E tests as well.

Fixing the Application

Through the changes we made in BookDataService in the previous section, our application, for the most part, no longer works. However, it is also clear that it is because all the BookDataService callers do not know yet how to deal with a returned promise.

In this section we want to show how the caller should handle the promise-based API of BookDataService to restore the functionality of the application. We will not discuss all the necessary changes in every single component. Rather, we will show two examples of how we bring the relevant components to life again.

In particular, we will not describe the failed test cases. In the accompanying zip file for the book we have naturally resolved all the existing problems in the project.

Based on the two examples we now present, you should know what changes to make to the calling components to get the entire application up and running again.

We assume that you have dealt with promises. If that is not the case, you can review the necessary information in Chapter 7, “Frequently Asked Questions.”

Fixing the Edit Function

We have to make the necessary changes at the points where we call an API function of BookDataService. As an example, we have picked the AdminEditBookCtrl controller as shown in Listing 4.47, because that is where the most changes have to be carried out.

Specifically, it deals with calls to the getBookByIsbn() function. Previously, this function returned the relevant Book object directly. From now on, we can assign the data to the scope variable book. We can do this since this function now returns a promise. To be able to respond to the arrival of an HTTP response, the promise offers us the then() function. This function expects as the first parameter the so-called success function that will be executed when the promise is resolved. As such, we include a successful HTTP response from the server. This function includes as a parameter a Response object representing the HTTP response. Using the object we can especially include the user data we received to access to the data property. If the user data in this case comes from a Book object, we can directly assign res.data to the scope variable book in the success function.

We can pass a second parameter to the then() function. The second parameter is an error function that will be called when the promise is rejected or when an error occurs in the HTTP request. The error function includes as a parameter an Error object that tells us what has gone wrong. In this case, we simply show the error on the console. Of course, we could write a more intelligent error handler here.

Listing 4.47: The necessary changes to the AdminEditBookCtrl controller when calling the getBookByIsbn() function

bmApp.controller('AdminEditBookCtrl',
function ($scope, $routeParams, $location, BookDataService) {
    $scope.isEditMode = true;
    $scope.submitBtnLabel = 'Buch editieren';

    var isbn = $routeParams.isbn;
    BookDataService.getBookByIsbn(isbn).then(function(res) {
        $scope.book = res.data;
    }, function(error) {
        console.log('An error occurred!', error);
    });

    [...]
});

When calling the updateBook() function in Listing 4.48, we have to do exactly this. We cannot directly call the goToAdminListView() function after calling updateBook(). Instead, we can first make this call when the server has successfully responded and the corresponding book was also updated in the server.

Listing 4.48: The necessary changes in the AdminEditBookCtrl controller when calling the updateBook() function

bmApp.controller('AdminEditBookCtrl',
function ($scope, $routeParams, $location, BookDataService) {
    [...]

    $scope.submitAction = function() {
        BookDataService.updateBook($scope.book).then(function() {
            goToAdminListView();
        }, function(error) {
            console.log('An error occurred!', error);
        });
     };

    $scope.cancelAction = function() {
        goToAdminListView();
    };
   
    var goToAdminListView = function() {
        $location.path('/admin/books'),
    };
});

For now that is everything we have to do to make the Book Edit view work again.

Repairing the tokenfield directive

In addition to the AdminEditBookCtrl controller, we want to look at the tokenfield directive, because asynchrony has double impacts in this component.

As you know, the tokenfield directive relies on the BookDataService to access all the available tags in the system to fill the auto-completion of the tokenfield plugins. Since now the getTags() function returns a promise, we can only execute the actual initialization of the tokenfield plugins in the success function when we have populated the array with tags from the server, as shown in Listing 4.49. In order to structure the source code a bit better, we have defined the local function initializeTokenfield for the initialization routine. We call this function in the success function and pass to it the included user data from the server that only exists in the tags array.

Listing 4.49: The necessary changes in calling getTags() in the tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        [...]
        link: function(scope, elem) {
            var initialized = false;
 
            // Fetch the tags from the server
            // and initialize directive.
            BookDataService.getTags().then(function(res) {
                initializeTokenfield(res.data);
            }, function(error) {
                console.log('An error occurred!', error);
            });

            // Main initialization routine
            function initializeTokenfield(tokens) {
                elem.tokenfield({
                    autocomplete: {
                        source: tokens,
                        delay: 100
                    },
                    [...]
                }).on('afterCreateToken', function (e) {
                    addToken(e.token.value);
                }).on('removeToken', function (e) {
                    removeToken(e.token.value);
                });

                function addToken(token) { [...] }

                function removeToken(token) { [...] }

                [...]
             }
        }
    }
});

The actual initialization logic now looks like the previous version of the tokenfield directive. However, there is a difference and this difference is shown in Listing 4.50. The difference is the initializeTagsArray() function, which in the previous section was called init().

Listing 4.50: The necessary changes in the tokenfield directive

bmApp.directive('tokenfield', function(BookDataService) {
    return {
        [...]
        link: function(scope, elem) {
            var initialized = false;

            [...]

            // Main initialization routine
            function initializeTokenfield(tokens) {
                [...] 

                function initializeTagsArray() {
                    if (angular.isUndefined(scope.tokenfield)) {
                        scope.$watch('tokenfield', function(book) {
                            if (!initialized && 
                              angular.isDefined(book)) {
                                elem.tokenfield('setTokens', 
                                  book.tags);
                                initialized = true;
                             }
                         });
                     }
                     else {
                         if (angular.isUndefined
                           (scope.tokenfield.tags))
                         {
                             scope.tokenfield.tags = [];
                         }
                         else {
                             elem.tokenfield('setTokens', 
                               scope.tokenfield.tags);
                         }

                         initialized = true;
                     }
                }

                initializeTagsArray();
            }
        }
    }
});

The directive now has to deal with the fact that the Book object from the parent scope (e.g., the scope of the AdminEditBookCtrl controller) that we can obtain from the two-way bound reference scope.tokenfield is also asynchronously loaded from the server. For this reason, we have to register a one-time watcher in case the Book object is not yet available. Recall that we have also employed an observer in our color picker to be notified of changes in scope variables and to be able to respond to them. In case scope.tokenfield is still undefined, here we proceed almost exactly in this way: we register an observer using scope.$watch(). However, we do not want to listen to changes in scope.tokenfield all the time, only until the scope variable book has been assigned. Therefore, we call this pattern a one-time watcher. Using the initialized flags, we can make sure the token assignment is executed only once when the Book object is eventually available.

In case the Book object is already available, we have to check if the tags array already exists (e.g., editing) or still has to be created (e.g., creating a new book) before the initializeTagsArray function is called.

After completing these changes, we have made the tokenfield directive work again.

BookMonkey with Backend Communication

Since we have worked to make sure all components that call an API function of BookDataService work again, we can now execute the entire project. In order to do that, we have to start the server with the following console command:

node server.js

Additionally, we have to ensure that the correct server URL is used.

srv._baseUrl = 'http://localhost:4730';

Like before, we deliver our AngularJS application with the http-server module. That results in our application handling all data access with an HTTP request to the server. We know that by showing the execution in Chrome in the Network tab. The HTTP requests our application sends to the backend should be listed there (See Figure 4.14).

Figure 4.14: The Network tab in Chrome

Summary

  • You can use the $http service to communicate with a REST web service easily.
  • In addition to the $http() base function, the service offers help functions that correspond to specific HTTP methods (GET, POST, etc.). These help functions are $http.get(), $http.post() and so on.
  • The $http service always returns a promise and a caller can use the then() function to register a success or error function as a callback function.
  • Using a promise, you can deal with asynchrony elegantly.
  • The application code that relies on the $http service is easy to test because AngularJS provides the $httpBackend mock object in the ngMock module.
  • Using this mock object you can formulate request-expectations and set predefined HTTP responses.
..................Content has been hidden....................

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