The ngOptions directive in AngularJS allows you to build and bind an HTML select element with options to a model property. It’s quite a versatile directive and is smarter than trying to build the select options with ngRepeat since ngOptions is optimized for two way data binding.
There are some tricks, however.
Let’s start with the following simple controller in JavaScript.
var EngineeringController = function($scope) { $scope.engineer = { name: "Dani", currentActivity: "Fixing bugs" }; $scope.activities = [ "Writing code", "Testing code", "Fixing bugs", "Dancing" ]; };
The goal is to build a drop down list to select an engineer’s current activity.
<div data-ng-controller="EngineeringController"> {{engineer.name}} is currently: {{ engineer.currentActivity}} <div> Choose a new activity: <select data-ng-model="engineer.currentActivity" data-ng-options="act for act in activities"> </select> </div> </div>
One of the tricks to using ngOptions is figuring out the expression AngularJS expects. The current value of “act for act in activities” is telling AngularJS to use the value of each entry in the activities array. This syntax against a simple array of strings allows the select element to appear with the engineer’s current activity selected, and if the selected option changes the framework updates the engineer’s current activity (and vice versa).
The expression you can use with ngOptions can be quite a bit more advanced (which could be good or bad). For example, instead of using strings let’s use objects to represent an activity.
var EngineeringController = function($scope) { $scope.engineer = { name: "Dani", currentActivity: { id: 3, type: "Work", name: "Fixing bugs" } }; $scope.activities = [ { id: 1, type: "Work", name: "Writing code" }, { id: 2, type: "Work", name: "Testing code" }, { id: 3, type: "Work", name: "Fixing bugs" }, { id: 4, type: "Play", name: "Dancing" } ]; };
And we’ll change the ngOptions expression to build a label for the select option.
<select data-ng-model="engineer.currentActivity" data-ng-options="a.name +' (' + a.type + ')' for a in activities"> </select>
This produces:
Expressions can also create optgroup elements when using a group by in the expression.
<select ng-model="engineer.currentActivity" data-ng-options="a.name group by a.type for a in activities"> </select>
Which yields:
When we switched from an array of strings to activities as objects, we lost the ability for the select to show the starting currentActivity as initially selected. If the user selects a new activity the two-way data binding works and the currentActivity is set, but we lost the initial selection. This is because AngularJS is doing reference comparisons and employee.currentActivity might by “Fixing bugs”, but employee.currentActivity != activities[2], so the page starts with an empty selection in the drop down list.
This is a common occurrence since the engineer object and the activities list are most likely de-serialized from HTTP calls and will completely different object graphs. There is no good solution with the exiting ngOptions directive but to “fix up” the engineer once the engineer and all the possible activities are loaded. Something like the following (which could be shortened with a library like undercore.js).
for (var i = 0; i < $scope.activities.length; i++) { if ($scope.activities[i].id == $scope.engineer.currentActivity.id) { $scope.engineer.currentActivity = $scope.activities[i]; break; } }
What if you had objects representing activities but only wanted the Id property of the selected activity instead of the entire activity. The ngOptions expression can handle this scenario, too. Inside the controller would look like this:
$scope.engineer = { name: "Dani", currentActivityId: 3 }; $scope.activities = [ { id: 1, type: "Work", name: "Writing code" }, { id: 2, type: "Work", name: "Testing code" }, { id: 3, type: "Work", name: "Fixing bugs" }, { id: 4, type: "Play", name: "Dancing" } ];
And the expression would use a select as value syntax.
<select ng-model="engineer.currentActivityId" data-ng-options="a.id as a.name group by a.type for a in activities"> </select>
And that concludes this post on ngOptions. I hope you found the topic stimulating, and stayed on the edge of your seat until these final words.
Imagine you need to support multiple projects that retrieve “widgets” of different types from various data sources. You might start with some simple type definitions that build a core API around widgets and widget storage.
public class Widget { public int Key { get; set; } } public interface IWidgetStore { Widget GetWidget(int key); }
The IWidgetStore interface is needed so that reusable algorithms can work with widgets.
Given these definitions, someone can build a widget specific for their project.
public class NamedWidget : Widget { public string Name { get; set; } }
As well as a concrete class for storing widgets.
public class WidgetStore : IWidgetStore { public Widget GetWidget(int key) { return _store.Single(u => u.Key == key); } readonly List<NamedWidget> _store = new List<NamedWidget>(); }
But the friction in development will start because specific projects don’t want to work with Widget, they want to work with NamedWidget where business properties like Name are defined. If every IWidgetStore can only return Widget, every caller has to cast the return value of GetWidget to a type like NamedWidget.
var widget = (NamedWidget)store.GetWidget(id); var name = widget.Name;
One solution is to use an explicit interface definition to hide the IWidgetStore from application developers.
public class WidgetStore : IWidgetStore { public NamedWidget GetWidget(int key) { return _store.Single(u => u.Key == key); } Widget IWidgetStore.GetWidget(int key) { return GetWidget(key); } readonly List<NamedWidget> _store = new List<NamedWidget>(); }
Now the application code can work with NamedWidget and still pass the WidgetStore to other low level algorithms which will access the object through the IWidgetStore interface.
But, if the application is trying to stay flexible or testable it might want to continue working with widget stores through the IWidgetStore interface too, just like the reusable algorithms, and then the code would still need to cast the return value of GetWidget.
Another solution is to use a generic type parameter to parameterize a widget store with the desired type of widget.
public interface IWidgetStore<TWidget> { TWidget GetWidget(int key); }
Now a concrete widget store can implement the interface and return derived Widget objects.
public class WidgetStore<TWidget> : IWidgetStore<TWidget> where TWidget: Widget { public TWidget GetWidget(int key) { return _store.Single(u => u.Key == key); } readonly List<TWidget> _store = new List<TWidget>(); }
What makes it all work is the generic constraint where TWidget: Widget. The C# complier assumes a TWidget is of type Object unless told otherwise, so the LINQ query in GetWidget will fail with a compiler error because Object doesn’t have a Key property.
The generic constraint tells the C# compiler that a TWidget has to be a Widget, and since a Widget has to have a Key property the C# compiler is happy to compile the code.
The angular-mocks library we’ve been using in the previous posts make unit tests easier to write and includes some built-in mocks for Angular services like $browser, $log, and $httpBackend. However, mocks aren’t available for every service, and there are certainly other services you’ll want to fake to isolate code in a unit test.
For example, AngularStrap offers directives and services for integrating AngularJS with Bootstrap. One of the services is the $modal service that allows you to programmatically launch a modal dialog from inside a view model. Here is a controller that launches the dialog as soon as the controller itself is created.
(function (module) { "use strict"; var severSelectDialog; var initServerSelectDialog = function ($modal, $scope) { return $modal( { template: '_serverSelect.html', persist: true, show: false, scope: $scope }); }; var showSelectServerDialog = function () { severSelectDialog.then(function (modal) { modal.modal("show"); }); }; var MainController = function ($scope, $modal) { severSelectDialog = initServerSelectDialog($modal, $scope); showSelectServerDialog(); // ... }; MainController.$inject = ["$scope", "$modal"]; module.controller("MainController", MainController); }(angular.module("mongoMagno")));
Using $modal to open a dialog is a two step process. First you call $modal and get a promise, then you call “show” on the modal object resolved by the promise (the promise exists because the markup template for the modal might be asynchronously loaded from the server).
Using Jasmine, if you wanted to unit test the controller to ensure the controller interacts with the $modal appropriately, you could use a spy with some fakes like so:
describe('MainController', function() { var fakeModal = { command: null, modal: function(command) { this.command = command; } }; var fakeModalPromise = { then: function(callback) { callback(fakeModal); } }; var $modal, scope; beforeEach(angular.mock.inject(function($rootScope, $controller) { fakeModal.command = null; $modal = jasmine.createSpy("$modal").andReturn(fakeModalPromise); scope = $rootScope.$new(); $controller('MainController', { $scope: scope, $modal: $modal }); })); it("should show server connect dialog when launched", function() { expect(fakeModal.command).toBe("show"); }); });
Another approach is not to use a fake promise but a real promise created by the Angular $q service.
describe('MainController', function() { var FakeModal = function() { this.modal = function(command) { this.command = command; }; }; var scope, fakeModal; beforeEach(angular.mock.inject(function($rootScope, $controller, $q) { var deferred = $q.defer(); fakeModal = new FakeModal(); deferred.resolve(fakeModal); $modal = jasmine.createSpy("$modal").andReturn(deferred.promise); scope = $rootScope.$new(); $scope.$apply(function() { $controller('MainController', { $scope: scope, $modal: $modal }); }); })); it("should show connect dialog when launched", function() { expect(fakeModal.command).toBe("show"); }); });
This approach requires a call to $scope.$apply (a real promise from the $q service doesn’t fully resolve until an $apply cycle executes).
In the last post we tested a simple controller, so now let’s look at a controller that likes to communicate over the network.
(function (module) { var MoviesController = function ($scope, $http) { $http.get("/api/movies") .then(function (result) { $scope.movies = result.data; }); }; module.controller("MoviesController", ["$scope", "$http", MoviesController]); }(angular.module("myApp")));
If you want a unit test to execute without network communication there are a couple options. You could refactor the controller to use a custom service to fetch movies instead of using $http directly. Then in a unit test it would be easy to provide a fake service that returns pre-canned responses.
Another option is to use angular-mocks. Angular mocks includes a programmable fake $httpBackend that replaces the real $httpBackend service. It is important to note that $httpBackend is not the same as the $http service. The $http service uses $httpBackend to send HTTP messages. In a unit test, we’ll use the real $http service, but a fake $httpBackend programmed to respond in a specific way.
Here is one approach:
describe("myApp", function () { beforeEach(module('myApp')); describe("MoviesController", function () { var scope, httpBackend; beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) { scope = $rootScope.$new(); httpBackend = $httpBackend; httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]); $controller('MoviesController', { $scope: scope, $http: $http }); })); it("should have 3 movies", function () { httpBackend.flush(); expect(scope.movies.length).toBe(3); }); }); });
In the above test, $httpBackend is programmed (with the when method) to respond to a GET request with three objects (empty objects, since the controller in this example never uses the data, but only assigns the response to the model).
For the data to arrive in the model, the test needs to call the flush method. Flush will respond to all requests with the programmed responses. The flush method will also verify there are no outstanding expectations. Writing tests with expectations is a little bit different.
describe("myApp", function () { beforeEach(module('myApp')); describe("MoviesController", function () { var scope, httpBackend, http, controller; beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) { scope = $rootScope.$new(); httpBackend = $httpBackend; http = $http; controller = $controller; httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]); })); it('should GET movies', function () { httpBackend.expectGET('/api/movies'); controller('MoviesController', { $scope: scope, $http: http }); httpBackend.flush(); }); }); });
The expectation in the above code is registered with the expectGET method. When the test calls flush, flush will fail the test if the controller did not make all of the expect calls. Of the two tests in this post, I prefer the first style. Testing with expectations is the same as interaction testing with mocks, which I’ve learned to shy away from because interaction testing tends to be brittle.
One of the benefits of using AngularJS is the ability to unit test the JavaScript code in a complex application. Unit testing is incredibly easy for trivial cases when controllers and models are declared in global scope. However unit testing is slightly more challenging for objects defined inside of Angular modules because of the need to bootstrap modules, work with a dependency injector, and deal with the subtleties of nested functional code.
Let’s try to test the following controller defined in a module:
(function (app) { var SimpleController = function ($scope) { $scope.x = 3; $scope.y = 4; $scope.doubleIt = function () { $scope.x *= 2; $scope.y *= 2; }; }; app.controller("SimpleController", ["$scope", SimpleController]); }(angular.module("myApp")));
We’ll be using AngularJS mocks and Jasmine in an HTML page, which requires the following scripts:
- jasmine.js
- jasmine-html.js
- angular.js
- angular-mocks.js
- simpleController.js (where the controller lives)
It’s important to include the Jasmine scripts before including angular-mocks, as angular-mocks will enable some additional features when Jasmine is present (notably the helper methods module and inject).
describe("myApp", function() { beforeEach(module('myApp')); describe("SimpleController", function() { var scope; beforeEach(inject(function($rootScope, $controller) { scope = $rootScope.$new(); $controller("SimpleController", { $scope: scope }); })); it("should double the numbers", function() { scope.doubleIt(); expect(scope.x).toBe(6); }); }); });
The module method used in the first beforeEach (which you can also invoke as angular.mocks.module) will initialize and configure the myApp module and its dependencies.
The inject method in the second beforeEach (angular.mocks.inject) takes a function that requires dependencies. Behind the scenes, the inject method will set up an $injector and use it to invoke the function with the right dependencies. In this test, we need the application’s $rootScope object and the $controller service. We are going to use the $controller service to have complete oversight over the instantiation of the SimpleController and keep a scope object around that we can write asserts against.
Once the setup code is done, the it tests are relatively easy to write (and read). The trick is figuring out what to inject, what to instantiate directly, and (a topic for future posts) what to mock or fake by hand.
Most applications feature menu items that will need to appear selected at the appropriate time.
How to do this with Angular?
There are many different approaches, but ideally we’ll do this with a directive, since directives are responsible for DOM manipulation, and “selecting” a menu item will require some manipulation of the classes on an element. The goal would be to make it as easy as possible from a view:
<nav> <ul data-active-menu="selected"> <li><a href="#/details?q=foo">Details</a></li> <li><a href="#/test">Summary</a></li> ... </ul> </nav>
In the above code, the data-active-menu attribute should allow a custom directive to jump in and manipulate the styles of the elements inside by adding “selected” as a class to the active link, and removing the class from all other links.
The directive code itself would look like this:
(function () { var makeWatcher = function(location) { return function() { return location.url(); }; }; var makeLinkUpdater = function(links, className) { return function (value) { angular.forEach(links, function(link) { link = angular.element(link); if (/\#(\/[^\/]+)/.exec(link.attr("href"))[1] == value) { link.addClass(className); } else { link.removeClass(className); } }); }; }; var activeMenu = function($location) { var link = function(scope, element, attrs) { var links = element.find("a"); var className = attrs.activeMenu; scope.$watch(makeWatcher($location), makeLinkUpdater(links, className)); }; return { link:link }; }; activeMenu.$injector = ["$location"]; angular.module("testApp") .directive("activeMenu", activeMenu); }());
By taking a dependency on the $location service in AngularJS, the directive can both watch for when the location changes, and compare the current location to the href in the menu items. Matching hrefs get the class specified in the data-menu-active attribute. Easy, reusable, and keeps more boilerplate code out of the controllers.
The last post demonstrated a service to wrap geolocation APIs in the browser.
At some point the application might need to add some logging before and after the geolocation calls. One way to do this is to add code to the geo service directly, but AngularJS also allows us to register decorators for any service (including the built-in Angular services).
Decorator is a wonderful pattern for extensibility since the original object doesn’t need to anything about the decoration, and neither does the consumer of the service.
Here is a quick example of creating a decorator for the geo service. The decorator will log the amount of time taken for the geo service to return a result. When creating a decorator, AngularJS will pass the original service as the $delegate parameter.
(function() { var geoDecorator = function($delegate) { var locate = function() { var start = new Date(); var result = $delegate.locate(); result.always(function () { console.log("Geo location took: " + (new Date() - start) + "ms"); }); return result; }; return { locate: locate }; }; var testApp = angular.module("testApp"); testApp.config(["$provide", function ($provide) { $provide.decorator("geo", geoDecorator); }]); }());