tech blog

AppNexus is today’s most powerful, open, and customizable ad tech platform. Advertising’s largest and most innovative companies build their businesses on AppNexus.

AngularJS blog series – MVC in AngularJS

| Comments

At AppNexus, we have been using the JavaScript framework AngularJS. We like it, and we’d like to share what it is and how to use it.

This is one in a series of posts about AngularJS. Here is the general introduction to AngularJS and the series list.

To illustrate AngularJS we will build a (very) simple user subscription feature similar to one we implemented for the AppNexus Twixt app. The UI will display a list of candidates in a dropdown and the user can select one or more candidates to be subscribers. Subscribers are then added to the subscriber list table, and removed from the candidate list. Unsubscribing a subscriber adds them back into the candidate list.

It looks like this:

mock.png

The Model, View, and Controller for this feature are built as follows:

  • model – a list of candidates with two key properties: name (string) and isSubscriber (boolean)
  • view – simple HTML layout of the form
  • controller – called SubscriptionController

Next we will walkthrough creating the view, controller, simple view model and supporting service.

For those who can’t wait, click here to see the implementation at the top of the page. Below it is a split screen display of the javascript code and accompanying comments. You’ll have to inspect to see the HTML.

Create the View (HTML)

The HTML for the dropdown of participants is as follows:

1
2
3
4
<select
	ng-model="model.selected"
	ng-options="participant.name for participant in model.participants | filter:{isSubscriber: false} | orderBy:'name'">
	</select>

You can see this is an HTML select box. But what’s with the ng-model and ng-options?

Well, Angular redefined the HTML <select> and basically superpowered it. This is called a Directive in Angular, and is a very powerful feature of Angular because you can (and will) create your own custom Directives too.

In the built-in select Directive you can:

  • Specify a model property in ng-model to hold the current selection
  • Use an iterator to build the list of options
  • Add an Angular filter, to adjust the iterator output. In our case the filter only shows participants who are not subscribers (isSubscriber === false) and sorts by name

Is your mind blown yet?

I’d note that directives are similar to the W3C Web Components draft standard, and the AngularJS team has plans to support this standard. Twixt makes extensive use of built-in and custom directives and directives will be addressed further in a forthcoming blog post.

Now to display the list of subscribers, iterate over the same model list (model.participants) but this time show only participants who are subscribers (isSubscriber === true). Here’s the key HTML:

1
2
3
  <tr ng-repeat="subscriber in subscribers = (model.participants | filter:{isSubscriber: true})">
	<td></td>
	</tr>

Notice the ng-repeat?

AngularJS defines a general-purpose iterator, ng-repeat, which is used to iterate over the list of participants. This subscriber list is also filtered for participants who are subscribers (the opposite of the previous list).

The is an example of an AngularJS data binding and will result in the subscribers name being displayed. Data binding syncs the UI to the model. If the name is changed in the model then the UI would update “automagically” to show it. That’s AngularJS data binding.

The reader may notice only one model (model.participants) is used by both lists. By toggling a property isSubscriber for each participant, it will appear/disappear in the appropriate list.

Angular is smart enough to detect model changes and automatically keep the lists updated (as subscribers are  added/removed). More may be said on this topic, but that is a blog post topic in itself.

The HTML view includes instructions which tells Angular to use the SubscriptionController from the subscriptions app for this part of the page:

1
2
<div ng-app="subscriptions">
<div ng-controller="SubscriptionController" class="form">

This is a way, but not the only way to wire up controllers and views.

 

Write Controller and Service

Define a Module

Angular has a module system to enable you to define your own modules, build your app from modules, declare module dependencies and include modules others have written.  “You can think of a module as a container for the different parts of your app – controllers, services, filters, directives, etc.”

Here we define an AngularJS module named ‘subscriptions’ to contain our subscriptions feature code as follows:

1
	angular.module('subscriptions', [])

Note the empty array list [] at the end. One can list other modules as dependencies there.

Define a Controller

AngularJS supports defining controllers (the C in MVC). Angular controllers work like typical MVC controllers: make data available to the view and respond to events. One significant difference between Angular controllers and many other JavaScript framework/library controllers is that you don’t manipulate the browsers Document Object Model (DOM) in an Angular controller. Instead, data binding or directives are used to modify the DOM. A major reason for this is to make AngularJS code much more testable by decoupling the controller code from the dom.

Next we define a SubscriptionController within the subscriptions module to handle loading the data and responding to UI events such as unsubscribe/subscribe and save changes.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
      angular.module('subscriptions', [])
      .controller('SubscriptionController', ['$scope', 'SubscriptionService',
       function($scope, SubscriptionService) {
        // $scope.model is an object that will be a holder for data to/from the view;
        // $scope is an object automatically provided by angular per controller instance
        $scope.model = {};

        // call the service to load the data, when loaded then provide the
        // participant list to the view via $scope.model (an angular pattern)
        SubscriptionService.getParticipants()
          .then(function(participants) {
            $scope.model.participants = participants;
          });

        // listen for user selecting a participant to be a subscriber;
        // $watch sets up a listener callback whenever a watchExpression changes
        $scope.$watch("model.selected", function(newValue, oldValue) {
          if (newValue === oldValue || newValue == null) return;

          // make the selected participant a subscriber
          $scope.model.selected.isSubscriber = true;
        });

        // user unsubscribed a subscriber, update the subscriber to no longer
        // be a subscriber
        $scope.removeSubscriber = function(subscriber) {
          subscriber.isSubscriber = false;

          $scope.model.selected = null; // reset choice
        };

        // user choose to save changes so call service to save subscriber list
        $scope.saveChanges = function() {
          SubscriptionService.updateParticipants($scope.model.participants)
            .then(function() {
              // handle save success
            },
            function() {
              // handle save failure
            });
        }

      }])

Note the SubscriptionController declares a dependency on a SubscriptionService which will be defined next. Because of this declared dependency, Angular will make the SubscriptionService available to the controller. This is Angulars Dependency Injection in action and is a powerful enabler for unit testing because it enables substitution of mock objects for real objects while unit testing (thus allowing testing in isolation).

Define a Service

AngularJS provides a built in abstraction layer called Services, to allow you “organize and share code across the app”. A service can be whatever you want it to be, from an API layer to a cache; AngularJS itself provides a number of useful pre-built services that are used in Twixt.

Define a SubscriptionService for use by the Controller (a common use case)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
      .service('SubscriptionService', ['$q', function($q) {
        // return a list of participants from api
        this.getParticipants = function() {

          // resolve the promise immediately with dummy data; but
          // typically would call to ajax api to get list of participants
          var defer = $q.defer();
          defer.resolve([{name: "Jane", id: 1, isSubscriber: false },
                {name: "John", id: 2, isSubscriber: false },
                {name: "Anne", id: 3, isSubscriber: false },
                {name: "Bob", id: 4, isSubscriber: false },
                {name: "Luther", id: 5, isSubscriber: false },
                {name: "Clarissa", id: 6, isSubscriber: false }
                ]);

          return defer.promise;
        };

        // add/remove subscribers by call to api
        this.updateParticipants =function(participants) {
          // no implementation here, just return a resolved promise
          var defer = $q.defer();
          defer.resolve();
          return defer.promise;
        };

      }])

Here’s the implementation code and working feature. Below it is a split screen display of the javascript code and accompanying comments. You’ll have to inspect to see the HTML.

Unit Testing

No introduction to AngularJS would be complete without highlighting Angulars support for testing. The Angular team strongly believes in robust testing and the framework is engineered to support it through patterns such as Dependency Injection, rules about DOM manipulation, mock services and APs such as expects which return dummy data for HTTP requests when testing.

For unit testing the AngularJS team created Karma, a test runner. Its easy to setup and you can write tests using the describe syntax with Jasmin/Mocha etc. Twixt has over 1300 unit tests and counting.

Here’s an outline of what a controller unit test could be for SubscriptionController (but it is contrived and is not complete)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
        describe('SubscriptionController', function() {
          var scope = null;

          beforeEach(inject(function($httpBackend, $rootScope, $controller) {
            scope = $rootScope.$new();

            // instantiate the controller (which will trigger a load of the participants)
            $controller('SubscriptionController', {$scope: scope});
          }));

          it('should be initialized with participants', function() {
            // expect we'll have at least one
            expect(scope.model.participants.length).toBeGreaterThan(0);
          });

          it('should be able to add subscriber', function() {
            // confirm first participant is not a subscriber
            expect(scope.model.participants[0].isSubscriber).toBeFalsy();

            // set selected to first participant and thus make them a subscriber
            scope.model.selected = scope.model.participants[0];

            // trigger the angular watch
            scope.$digest();

            // confirm first participant is indeed now a subscriber
            expect(scope.model.participants[0].isSubscriber).toBeTruthy();
          });
        });

For e2e functional testing the Angular team uses Protractor. Protractor uses WebDriver on top of Selenium. One nice feature is one can use the same syntax to write e2e tests as for unit tests. Twixt has over 1500 e2e tests and all run on Chrome, FF, IE10 and 11 (a subset run on Safari and IE9). These can be run asynchronously.

e2e testing is basically simulating an end user, logging into the app, navigating to pages, entering data etc. Its pretty awesome to see it when it runs and something we’d like to demo in the future.

Well, thats it. Our whirlwind tour covered Model View Controller in angularjs as well as briefly touching on Directives, modules, testing and Dependency Injection. Let us know your thoughts and feedback on AngularJs in the comments.