Chapter 4. AngularJS Controllers

AngularJS controllers are at the center of AngularJS applications and are probably the most important component to understand. Controllers are not always clearly defined in some JavaScript client-side frameworks, and that tends to confuse developers who have experience with MVC frameworks. That is not the case with AngularJS. AngularJS clearly defines controllers, and controllers are at the center of AngularJS applications.

Almost everything that happens in an AngularJS application passes through a controller at some point. Dependency injection is used to add the needed dependencies, as shown in the following example file, which illustrates how to create a new controller:

/* chapter4/controllers.js - a new controller */

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

addonsControllers.controller('AddonsCtrl', 
  ['$scope', 'checkCreds', '$location', 'AddonsList', '$http', 'getToken',
    function AddonsCtrl($scope, checkCreds, $location, AddonsList, 
      $http, getToken) {
        if (checkCreds() !== true) {
            $location.path('/loginForm');
        }

        $http.defaults.headers.common['Authorization'] = 
          'Basic ' + getToken();
        AddonsList.getList({},
           function success(response) { 
              console.log("Success:" + 
                     JSON.stringify(response));
                    $scope.addonsList = response;
           },
           function error(errorResponse) {
              console.log("Error:" + 
                     JSON.stringify(errorResponse));                   
           }
        );
        $scope.addonsActiveClass = "active";
}]);

In this code, we first create a new module named addonsController by making a call to the module method of angular. On the second line, we create a new controller named AddonsCtrl by calling the controller method of the addonsControllers module. Doing that attaches the new controller to that module. All controllers created in the controllers.js file will be added to the addonsControllers module.

Also notice the line console.log("Success:" + JSON.stringify(response)). Most modern browsers have accompanying developer tools that give developers easy access to the JavaScript console. This line uses the JSON.stringify method to log the JSON that’s returned from the web service to the JavaScript console. Developers can easily use the JavaScript console to troubleshoot REST service issues by viewing the JSON logged in the success callback function, or in the error callback function if a service call fails.

Most developer tools and some IDEs, like NetBeans, also include JavaScript debuggers that allow developers to place breakpoints in both the success and error callback functions. Doing so allows the developer to take a fine-grained approach to troubleshooting REST services. Quite often, the developer can resolve otherwise complex REST service issues very quickly by using a JavaScript debugger.

The following code is an excerpt of the previous file. It shows how we use dependency injection to add dependencies to the new controller. This code shows $scope, checkCreds, $location, AddonsList, $http, and getTokens as dependencies for the new controller. We have already covered the $scope briefly. For now it’s not important what the other dependencies actually represent; you only need to understand they are required by the new controller:

/* chapter4/controllers.js excerpt */
/* using dependency injection */

['$scope', 'checkCreds', '$location', 'AddonsList', '$http', 'getToken',
  function AddonsCtrl($scope, checkCreds, $location, AddonsList,
    $http, getToken) {
}

This controller plays a major role in the application in which it was defined. Controllers really have two primary responsibilities in an application. We will take a look at those responsibilities in more detail in the next section.

Initializing the Model with Controllers

AngularJS controllers have two primary duties in an application. First, controllers should be used to initialize the model scope properties. When a controller is created and attached to the DOM, a child scope is created. The child scope holds a model used specifically for the controller to which it is attached. You can access the child scope by using the $scope object.

Create a copy of the Chapter 2 project and name it AngularJsHelloWorld_chapter4. We will use this new project for the rest of this chapter. You can also download the project from the GitHub project site.

Model properties can be added to the scope, and once added they are available inside the view templates. The controller code shown here illustrates how to add two properties to the scope. After adding the customer name and customer number to the scope, both are available to the view and can be accessed with double curly braces:

/* chapter4/controllers.js excerpt */

helloWorldControllers.controller('CustomerCtrl', ['$scope', 
function CustomerCtrl($scope) { 
    $scope.customerName = "Bob's Burgers"; 
    $scope.customerNumber = "44522"; 
}]); 

Now add the new controller, CustomerCtrl, to your project’s controllers.js file. We will make several additions to the controllers.js file in this chapter.

The following view template code shows how to access the newly added model properties inside the view template. All properties that need to be accessed from the view should be added to the $scope object:

<!-- chapter4/partials/customer.html -->

<div><b>Customer Name:</b> {{customerName}}</div>
<div><b>Customer Number:</b> {{customerNumber}}</div> 

Now add a new HTML file under the partials folder and name it customer.html. Replace the generated code with the code just shown.

Adding Behavior with Controllers

The second primary use for controllers is adding behavior to the $scope object. We add behavior by adding methods to the scope, as shown in the following controller code. Here, we attach a changeCustomer method to $scope so that it can be invoked from inside the view. By doing this, we are adding behavior that allows us to change the customer name and customer number:

/* chapter4/controllers.js excerpt */

helloWorldControllers.controller('CustomerCtrl', ['$scope',
function CustomerCtrl($scope) {

    $scope.customerName = "Bob's Burgers";
    $scope.customerNumber = 44522;
    
    // add method to scope
    $scope.changeCustomer = function(){
      $scope.customerName = $scope.cName;
      $scope.customerNumber = $scope.cNumber;
    };

}]);

Add the changeCustomer method shown here to the CustomerCtrl controller defined in your controllers.js file.

The following code shows the customer.html file and the changes needed in the view to make use of the new behavior that was just added. We add two new properties to the model by using ng-model="cName" and ng-model="cNumber". We use ng-click="changeCustomer();" to invoke the new changeCustomer method that is attached to the scope:

<!-- chapter4/partials/customer.html -->

<div><b>Customer Name:</b> {{customerName}}</div>
<div><b>Customer Number:</b> {{customerNumber}}</div>

<form>

  <div>
    <input type="text" ng-model="cName" required/>
  </div>

  <div>
    <input type="number" ng-model="cNumber" required/>
  </div>

  <div>
    <button ng-click="changeCustomer();" >Change Customer</button>
  </div>

</form> 

Modify the customer.html file to include the new form defined here.

Once the changeCustomer method is invoked, the new properties are attached to $scope and available to the controller. As you can see, we simply assign the two new properties bound to the model back to the original two properties, customerName and customerNumber, inside the changeCustomer method. Both ng-model and ng-click are AngularJS directives. We will cover directives in detail in Chapter 9.

Form Submission

Now we will look at how form submissions are handled in AngularJS using controllers. The following code for the newCustomer.html file shows the view for a new form. Create a new HTML file under the partials folder and replace the generated code with the code listed here:

<!-- chapter4/partials/newCustomer.html -->

<form ng-submit="submit()" ng-controller="AddCustomerCtrl">

  <div>
    <input type="text" ng-model="cName" required/>
  </div>

  <div>
    <input type="text" ng-model="cCity" required/>
  </div>

  <div>
    <button type="submit" >Add Customer</button>
  </div>

</form>

As you can see, we use standard HTML for the form with nothing really special except the directives. The directive ng-submit binds the method named submit, defined in the AddCustomerCtrl controller, to the form for form submission. The ng-model directive binds the two input elements to scope properties.

Two or more controllers can be applied to the same element, and we can use controller as to identify each individual controller. The following code shows how controller as is used. You can see that addCust identifies the AddCustomerCtrl controller. We use addCust to access the properties and methods of the controller, as shown:

<!-- chapter4/partials/newCustomer.html (with controller as) -->

<form ng-submit="addCust.submit()" 
         ng-controller="AddCustomerCtrl as addCust">

  <div>
    <input type="text" ng-model="addCust.cName" required/>
  </div>

  <div>
    <input type="text" ng-model="addCust.cCity" required/>
  </div>

  <div>
    <button id="f1" type="submit" >Add Customer</button>
  </div>

</form>

The following code shows the AddCustomerCtrl controller and how we use it to handle the submitted form data. Here we use the path method on the AngularJS service $location to change the path after the form is submitted. The new path is http://localhost:8383/AngularJsHelloWorld_chapter4/index.html#!/addedCustomer/name/city.

Add this code to the controllers.js file:

/* chapter4/controllers.js */

helloWorldControllers.controller('AddCustomerCtrl', 
['$scope', '$location',
  function AddCustomerCtrl($scope, $location) {
   $scope.submit = function(){
    $location.path('/addedCustomer/' + $scope.cName + "/" + $scope.cCity);
  };
}]); 

That’s all that is needed to handle the form substitution process. We will now look at how we get access to the submitted values inside another controller.

Using Submitted Form Data

The app.js file shown next includes the new route definitions. Modify the app.js file in the Chapter 3 project and add the new routes. Make sure your file looks like the file shown here:

/* chapter4/app.js */
/* App Module */

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

helloWorldApp.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
  $routeProvider.
  when('/', {
    templateUrl: 'partials/main.html',
    controller: 'MainCtrl'
  }).when('/show', {
    templateUrl: 'partials/show.html',
    controller: 'ShowCtrl'
  }).when('/customer', {
    templateUrl: 'partials/customer.html',
    controller: 'CustomerCtrl'
  }).when('/addCustomer', {
    templateUrl: 'partials/newCustomer.html',
    controller: 'AddCustomerCtrl'
  }).when('/addedCustomer/:customer/:city', {
    templateUrl: 'partials/addedCustomer.html',
    controller: 'AddedCustomerCtrl'
  });

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

You can see there are two path parameters, customer and city, for the addedCustomer route. The values are passed as arguments to a new controller, AddedCustomerCtrl, shown in the following excerpt. We use the $routeParams service in the new controller to get access to the values passed as path parameter arguments in the URL. By using $routeParams.customer we get access to the customer name, and $routeParams.city gets us access to the city:

/* chapter4/controllers.js excerpt */

helloWorldControllers.controller('AddedCustomerCtrl', 
['$scope', '$routeParams',
function AddedCustomerCtrl($scope, $routeParams) {

  $scope.customerName = $routeParams.customer;
  $scope.customerCity = $routeParams.city;

}]);

Add the new controller, AddedCustomerCtrl, to your controllers.js file now.

The code for our new addedCustomer template is shown next. Once again, we use AngularJS double curly braces to get access to and display both the customerName and customerCity properties in the view:

<!-- chapter4/addedCustomer.html -->

<div><b>Customer Name: </b> {{customerName}}</div>

<div><b>Customer City: </b> {{customerCity}}</div>

To add the template to the project, create a new HTML file in the partials folder and name it addedCustomer.html. Replace the generated code with the code just shown. Note how simple it is to submit forms with AngularJS. Simplicity is one of the factors that makes AngularJS a great choice for any JavaScript client-side application project.

JS Test Driver

The rest of this chapter will cover setting up a test environment and testing AngularJS controllers. NetBeans has a great testing environment for both JS Test Driver and Karma. We will focus first on setting up JS Test Driver for unit testing. We will then take a look at Karma for unit testing. To begin, do the following:

  1. Download the JS Test Driver JAR.
  2. In the Services tab, right-click “JS Test Driver” and click “Configure” (see Figure 4-1).
  3. Select the location of the JS Test Driver JAR just downloaded and choose the browser of your choice (see Figure 4-2).
  4. Right-click the project node, then click “New”→“Other”→“Unit Tests.”
  5. Select “jsTestDriver Configuration File” and click “Next.”
  6. Make sure the file is placed in the config subfolder, as shown in Figure 4-3.
  7. Make sure the checkbox for “Download and setup Jasmine” is checked.
  8. Click “Finish.”
  9. Right-click the project node, click Properties, and select “JavaScript Testing.”
  10. Select “jsTestDriver” from the drop-down box.
Alt Text
Figure 4-1. Right-click “JS Test Driver” in the Services tab
Alt Text
Figure 4-2. Select your browser(s)
Alt Text
Figure 4-3. Make sure the file is created in the config subfolder

The following code shows the JS Test Driver configuration file. Inside the file, we specify the server URL that is used by JS Test Driver. We also specify the needed library files in the load section of the file, along with the locations of our JavaScript files and test scripts:

/*  chapter4/jsTestdriver.conf */

server: http://localhost:42442
load:
- test/lib/jasmine/jasmine.js
- test/lib/jasmine-jstd-adapter/JasmineAdapter.js

- public_html/js/libs/angular.min.js
- public_html/js/libs/angular-mocks.js
- public_html/js/libs/angular-cookies.min.js
- public_html/js/libs/angular-resource.min.js
- public_html/js/libs/angular-route.min.js
- public_html/js/*.js

- test/unit/*.js
exclude:

Notice we’ve added angular-mocks.js to the list of required AngularJS library files. That file is needed for unit testing AngularJS applications. So, before continuing, add the angular-mocks.js file to the js/libs folder.

Creating Test Scripts

Next, create a new JavaScript file in the unit subfolder of the newly created Unit Test folder, as shown in Figure 4-4. Name the new file controllerSpec.js.

Alt Text
Figure 4-4. Create the controllerSpec.js file in the unit subfolder

The contents of the controllerSpec.js file are shown next. Our test script filename will end with Spec. The file specifies a standard set of unit tests commonly used to test AngularJS controllers. Notice that we have a test for each of our controllers defined in the controllers.js file:

/* chapter4/controllerSpec.js */

/* Jasmine specs for controllers go here */
describe('Hello World', function() {

  beforeEach(module('helloWorldApp'));

  describe('MainCtrl', function(){
    var scope, ctrl;
    beforeEach(inject(function($rootScope, $controller) {
      scope = $rootScope.$new();
      ctrl = $controller('MainCtrl', {$scope: scope});
    }));

    it('should create initialed message', function() {
      expect(scope.message).toEqual("Hello World");
    });

  });

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

    beforeEach(inject(function($rootScope, $controller) {
      scope = $rootScope.$new();
      ctrl = $controller('ShowCtrl', {$scope: scope});
    }));

    it('should create initialed message', function() {
      expect(scope.message).toEqual("Show The World");
    });

  });

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

    beforeEach(inject(function($rootScope, $controller) {
      scope = $rootScope.$new();
      ctrl = $controller('CustomerCtrl', {$scope: scope});
    }));

    it('should create initialed message', function() {
      expect(scope.customerName).toEqual("Bob's Burgers");
    });
  });
});

This test script uses Jasmine as the behavior-driven development framework for testing our code. We will use Jasmine for all our test scripts in this book.

Here is the complete controllers.js file:

/* chapter4/controllers.js */

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

helloWorldControllers.controller('MainCtrl', ['$scope',
  function MainCtrl($scope) {
    $scope.message = "Hello World";
}]);

helloWorldControllers.controller('ShowCtrl', ['$scope',
  function ShowCtrl($scope) {
    $scope.message = "Show The World";
}]);

helloWorldControllers.controller('CustomerCtrl', ['$scope',
  function CustomerCtrl($scope) {
    $scope.customerName = "Bob's Burgers";
    $scope.customerNumber = 44522;
    $scope.changeCustomer = function(){
    $scope.customerName = $scope.cName;
    $scope.customerNumber = $scope.cNumber;
  };
}]);

helloWorldControllers.controller('AddCustomerCtrl', 
['$scope', '$location',
  function AddCustomerCtrl($scope, $location) {
    $scope.submit = function(){
    $location.path('/addedCustomer/' + $scope.cName + "/" + $scope.cCity);
  };
}]);

helloWorldControllers.controller('AddedCustomerCtrl', 
['$scope', '$routeParams',
  function AddedCustomerCtrl($scope, $routeParams) {
    $scope.customerName = $routeParams.customer;
    $scope.customerCity = $routeParams.city;
}]); 
Tip

To save time, you can download the Chapter 4 code from GitHub. For a complete guide to JavaScript testing in NetBeans, see the documentation at on the NetBeans website.

Testing with Karma

Karma is a new and fun way to unit test AngularJS applications. We will use Karma here to test the controllers that we tested earlier.

Installing Karma

Karma runs on Node.js, as mentioned in Chapter 2, so first you must install Node.js if it’s not already installed. Refer to nodejs.org for installation details for your particular operating system. You’ll also need to install the Node.js package manager (npm) on your system. npm is a command-line tool used to add the needed Node.js modules to a project.

Now, in the root of the Chapter 4 project, create a JSON file named package.json and add the following content. The package.json file is used as a configuration file for Node.js:

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

Open a command-line window on your system, and navigate to the root of the Chapter 4 project. You should see the package.json file when you list out the files in the folder.

Type this command to actually install the Node.js dependencies defined in the package.json file:

npm install

Now install the Karma command-line interface (karma-cli) by typing the following command:

npm install -g karma-cli
Warning
Make sure to record the location where karma-cli was installed. You will need the location later in this chapter.

This command installs the command-line tool globally on your system. 

All the Node.js dependencies specified in the package.json file will be installed under the node_modules folder inside the project root folder. If you list out the files and folders, you should see the new folder. You won’t be able to see the new folder inside NetBeans, however.

Karma Configuration

Next, create a new Karma configuration file named karma.conf.js inside the project test folder. Do the following:

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

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

/* chapter4/karma.conf.js */

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

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

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

End-to-End Testing with Protractor

Protractor is a new test framework for running end-to-end (E2E) tests. Protractor lets you run tests that exercise the application as a user would. With Protractor E2E testing, you can test various pages, navigate through each page from within the test script, and find any potential defects. Protractor also integrates with most continuous integration build systems.

Configuring Protractor

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

/ *chapter4/conf.js */

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

Creating Protractor Test Specifications

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

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

Now copy the code shown here into the new Hw-spec.js file:

/* chapter4/Hw-spec.js Protractor test specification */

describe("Hello World Test", function(){
    it("should test the main page", function(){
        browser.get(
         "http://localhost:8383/AngularJsHelloWorld_chapter4/");
        expect(browser.getTitle()).toEqual("AngularJS Hello World");
        
        var msg = element(by.binding("message")).getText();
        expect(msg).toEqual("Hello World");        
        
        browser.get(
     "http://localhost:8383/AngularJsHelloWorld_chapter4/#!/show");
        expect(browser.getTitle()).toEqual("AngularJS Hello World");
        
        var msg = element(by.binding("message")).getText();
        expect(msg).toEqual("Show The World");        
        
        browser.get(
"http://localhost:8383/AngularJsHelloWorld_chapter4/#!/
addCustomer");               
        
        element(by.model("cName")).sendKeys("tester");
        element(by.model("cCity")).sendKeys("Atlanta");
        element(by.id("f1")).click();        
        
        browser.get(
"http://localhost:8383/
AngularJsHelloWorld_chapter4/#!/addedCustomer/tester/Atlanta");
        
        var msg = element(by.binding("customerName")).getText();
        expect(msg).toEqual("Customer Name: tester");
        
        var msg = element(by.binding("customerCity")).getText();
        expect(msg).toEqual("Customer City: Atlanta");
    });
});

Running Protractor

Now that the Selenium Server is running, we can run our Protractor tests. Open a new command window, navigate to the root of the Chapter 4 project, and type this command:

protractor test/conf.js

You should see a browser window open. You should then see the test script navigate through the pages of the Chapter 4 application. If you watch the browser window closely, you will see the script enter values in the form that adds a new customer. When the Protractor script has finished, the browser window will close.

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

Finished in 3.368 seconds
1 test, 6 assertions, 0 failures
Note
For more information on testing with Protractor, see the project site on GitHub. Protractor has a complete set of documentation to help you get started.