Skip to main content

3 posts tagged with "unit tests"

View All Tags

Unit Testing an Angular Controller with Jasmine

Anyone who reads my blog will know that I have been long in the habit of writing unit tests for my C# code. I'm cool like that. However, it took me a while to get up and running writing unit tests for my JavaScript code. I finally got there using a combination of Jasmine 2.0 and Chutzpah. (Jasmine being my test framework and Chutzpah being my test runner.)

I'm getting properly into the habit of testing my JavaScript. I won't pretend it's been particularly fun but I firmly believe it will end up being useful... That's what I tell myself during the long dark tea-times of the soul anyway.

I have a side project called Proverb. It doesn't do anything in particular - for the most part it's a simple application that displays the collected wise sayings of a team that I used to be part of. There's not much to it - a bit of CRUD, a dashboard. Not much more. Because of the project's simplicity it's ideal to use Proverb's underlying idea when trying out new technologies / frameworks. The best way to learn is to do. So if I want to learn "X", then building Proverb using "X" is a good way to go.

I digress already. I had a version of Proverb built using a combination of AngularJS and TypeScript. I had written the Angular side of Proverb without any tests. Now I was able to write JavaScript tests for my Angular code that's just what I set out to do. It should prove something of a of Code Kata too.

Whilst I'm at it I thought it might prove helpful if I wrote up how I approached writing unit tests for a single Angular controller. So here goes.

What I'm Testing#

I have an Angular controller called sagesDetail. It powers this screen:

sagesDetail is a very simple controller. It does these things:

  1. Load the "sage" (think of it as just a "user") and make it available on the controller so it can be bound to the view.
  2. Set the view title.
  3. Log view activation.
  4. Expose a gotoEdit method which, when called, redirects the user to the edit screen.

The controller is written in TypeScript and looks like this:

sagesDetail.ts#

module controllers {
"use strict";
var controllerId = "sageDetail";
interface sageDetailRouteParams extends ng.route.IRouteParamsService {
id: string;
}
class SageDetail {
log: loggerFunction;
sage: sage;
title: string;
static $inject = ["$location", "$routeParams", "common", "datacontext"];
constructor(
private $location: ng.ILocationService,
private $routeParams: sageDetailRouteParams,
private common: common,
private datacontext: datacontext
) {
this.sage = undefined;
this.title = "Sage Details";
this.log = common.logger.getLogFn(controllerId);
this.activate();
}
// Prototype methods
activate() {
var id = parseInt(this.$routeParams.id, 10);
var dataPromises: ng.IPromise<any>[] = [this.datacontext.sage.getById(id, true).then(data => this.sage = data)];
this.common.activateController(dataPromises, controllerId, this.title)
.then(() => {
this.log("Activated Sage Details View");
this.title = "Sage Details: " + this.sage.name;
});
}
gotoEdit() {
this.$location.path("/sages/edit/" + this.sage.id);
}
}
angular.module("app").controller(controllerId, SageDetail);
}

When compiled to JavaScript it looks like this:

sageDetail.js#

var controllers;
(function (controllers) {
"use strict";
var controllerId = "sageDetail";
var SageDetail = (function () {
function SageDetail($location, $routeParams, common, datacontext) {
this.$location = $location;
this.$routeParams = $routeParams;
this.common = common;
this.datacontext = datacontext;
this.sage = undefined;
this.title = "Sage Details";
this.log = common.logger.getLogFn(controllerId);
this.activate();
}
// Prototype methods
SageDetail.prototype.activate = function () {
var _this = this;
var id = parseInt(this.$routeParams.id, 10);
var dataPromises = [this.datacontext.sage.getById(id, true).then(function (data) {
return _this.sage = data;
})];
this.common.activateController(dataPromises, controllerId, this.title).then(function () {
_this.log("Activated Sage Details View");
_this.title = "Sage Details: " + _this.sage.name;
});
};
SageDetail.prototype.gotoEdit = function () {
this.$location.path("/sages/edit/" + this.sage.id);
};
SageDetail.$inject = ["$location", "$routeParams", "common", "datacontext"];
return SageDetail;
})();
angular.module("app").controller(controllerId, SageDetail);
})(controllers || (controllers = {}));
//# sourceMappingURL=sageDetail.js.map

Now for the Tests#

I haven't yet made the move of switching over my Jasmine tests from JavaScript to TypeScript. (It's on my list but there's only so many things you can do at once...) For that reason the tests you'll see here are straight JavaScript. Below you will see the tests for the sageDetail controller.

I have put very comments in the test code to make clear the intent to you, dear reader. Annotated the life out of them. Naturally I wouldn't expect a test to be so heavily annotated in a typical test suite - and you can be sure mine normally aren't!

Jasmine tests for sageDetail.js#

describe("Proverb.Web -> app-> controllers ->", function () {
// Before each test runs we're going to need ourselves an Angular App to test - go fetch!
beforeEach(function () {
module("app"); // module is an alias for <a href="https://docs.angularjs.org/api/ngMock/function/angular.mock.module">angular.mock.module</a>
});
// Tests for the sageDetail controller
describe("sageDetail ->", function () {
// Declare describe-scoped variables
var $rootScope,
getById_deferred, // deferred used for promises
$location, $routeParams_stub, common, datacontext, // controller dependencies
sageDetailController; // the controller
// Before each test runs set up the controller using inject - an alias for <a href="https://docs.angularjs.org/api/ngMock/function/angular.mock.inject">angular.mock.inject</a>
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _$location_, _common_, _datacontext_) {
// Note how each parameter is prefixed and suffixed with "_" - this an Angular nicety
// which allows you to have variables in your tests with the original reference name.
// So here we assign the injected parameters to the describe-scoped variables:
$rootScope = _$rootScope_;
$q = _$q_;
$location = _$location_;
common = _common_;
datacontext = _datacontext_;
// Our controller has a dependency on an "id" property passed on the $routeParams
// We're going to stub this out with a JavaScript object literal
$routeParams_stub = { id: "10" };
// Our controller depends on a promise returned from this function: datacontext.sage.getById
// Well strictly speaking it also uses a promise for activateController but since the activateController
// promise just wraps the getById promise it will be resolved when the getById promise is.
// Here we create a deferred representing the getById promise which we can resolve as we need to
getById_deferred = $q.defer();
// set up a spy on datacontext.sage.getById and set it to return the promise of getById_deferred
// this allows us to #1 detect that getById has been called
// and #2 resolve / reject our promise as our test requires using getById_deferred
spyOn(datacontext.sage, "getById").and.returnValue(getById_deferred.promise);
// set up a spy on common.activateController and set it to call through
// this allows us to detect that activateController has been called whilst
// maintaining existing controller functionality
spyOn(common, "activateController").and.callThrough();
// set up spys on common.logger.getLogFn and $location.path so we can detect they have been called
spyOn(common.logger, "getLogFn").and.returnValue(jasmine.createSpy("log"));
spyOn($location, "path").and.returnValue(jasmine.createSpy("path"));
// create a sageDetail controller and inject the dependencies we have set up
sageDetailController = _$controller_("sageDetail", {
$location: $location,
$routeParams: $routeParams_stub,
common: common,
datacontext: datacontext
});
}));
// Tests for the controller state at the point of the sageDetail controller's creation
// ie before the getById / activateController promises have been resolved
// So this tests the constructor (function) and the activate function up to the point
// of the promise calls
describe("on creation ->", function () {
it("controller should have a title of 'Sage Details'", function () {
// tests this code has executed:
// this.title = "Sage Details";
expect(sageDetailController.title).toBe("Sage Details");
});
it("controller should have no sage", function () {
// tests this code has executed:
// this.sage = undefined;
expect(sageDetailController.sage).toBeUndefined();
});
it("datacontext.sage.getById should be called", function () {
// tests this code has executed:
// this.datacontext.sage.getById(id, true)
expect(datacontext.sage.getById).toHaveBeenCalledWith(10, true);
});
});
// Tests for the controller state at the point of the resolution of the getById promise
// ie after the getById / activateController promises have been resolved
// So this tests the constructor (function) and the activate function after the point
// of the promise calls
describe("activateController ->", function () {
var sage_stub;
beforeEach(function () {
// Create a sage stub which will be used when resolving the getById promise
sage_stub = { name: "John" };
});
it("should set sages to be the resolved promise values", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
// tests this code has executed:
// this.sage = data
expect(sageDetailController.sage).toBe(sage_stub);
});
it("should log 'Activated Sage Details View' and set title with name", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
// tests this code has executed:
// this.log("Activated Sage Details View");
// this.title = "Sage Details: " + this.sage.name;
expect(sageDetailController.log).toHaveBeenCalledWith("Activated Sage Details View");
expect(sageDetailController.title).toBe("Sage Details: " + sage_stub.name);
});
});
// Tests for the gotoEdit function on the controller
// Note that this will only be called *after* a controller has been created
// and it depends upon a sage having first been loaded
describe("gotoEdit ->", function () {
var sage_stub;
beforeEach(function () {
// Create a sage stub which will be used when resolving the getById promise
sage_stub = { id: 20 };
});
it("should set $location.path to edit URL", function () {
// Resolve the getById promise with the sage stub
getById_deferred.resolve(sage_stub);
$rootScope.$digest(); // So Angular processes the resolved promise
sageDetailController.gotoEdit();
// tests this code has executed:
// this.$location.path("/sages/edit/" + this.sage.id);
expect($location.path).toHaveBeenCalledWith("/sages/edit/" + sage_stub.id);
});
});
});
});

Running JavaScript Unit Tests in AppVeyor

With a little help from Chutzpah...#

AppVeyor (if you're not aware of it) is a Continuous Integration provider. If you like, it's plug-and-play CI for .NET developers. It's lovely. And what's more it's "free for open-source projects with public repositories hosted on GitHub and BitBucket". Boom! I recently hooked up 2 of my GitHub projects with AppVeyor. It took me all of... 10 minutes. If that? It really is *that* good.

But.... There had to be a "but" otherwise I wouldn't have been writing the post you're reading. For a little side project of mine called Proverb there were C# unit tests and there were JavaScript unit tests. And the JavaScript unit tests weren't being run... No fair!!!

Chutzpah is a JavaScript test runner which at this point runs QUnit, Jasmine and Mocha JavaScript tests. I use the Visual Studio extension to run Jasmine tests on my machine during development. I've also been able to use Chutzpah for CI purposes with Visual Studio Online / Team Foundation Server. So what say we try and do the triple and make it work with AppVeyor too?

NuGet me?#

In order that I could run Chutzpah I needed Chutzpah to be installed on the build machine. So I had 2 choices:

  1. Add Chutzpah direct to the repo
  2. Add the Chutzpah Nuget package to the solution

Unsurprisingly I chose #2 - much cleaner.

Now to use Chutzpah#

Time to dust down the PowerShell. I created myself a "before tests script" and added it to my build. It looked a little something like this:

# Locate Chutzpah
$ChutzpahDir = get-childitem chutzpah.console.exe -recurse | select-object -first 1 | select -expand Directory
# Run tests using Chutzpah and export results as JUnit format to chutzpah-results.xml
$ChutzpahCmd = "$($ChutzpahDir)\chutzpah.console.exe $($env:APPVEYOR_BUILD_FOLDER)\AngularTypeScript\Proverb.Web.Tests.JavaScript /junit .\chutzpah-results.xml"
Write-Host $ChutzpahCmd
Invoke-Expression $ChutzpahCmd
# Upload results to AppVeyor one by one
$testsuites = [xml](get-content .\chutzpah-results.xml)
$anyFailures = $FALSE
foreach ($testsuite in $testsuites.testsuites.testsuite) {
write-host " $($testsuite.name)"
foreach ($testcase in $testsuite.testcase){
$failed = $testcase.failure
$time = $testsuite.time
if ($testcase.time) { $time = $testcase.time }
if ($failed) {
write-host "Failed $($testcase.name) $($testcase.failure.message)"
Add-AppveyorTest $testcase.name -Outcome Failed -FileName $testsuite.name -ErrorMessage $testcase.failure.message -Duration $time
$anyFailures = $TRUE
}
else {
write-host "Passed $($testcase.name)"
Add-AppveyorTest $testcase.name -Outcome Passed -FileName $testsuite.name -Duration $time
}
}
}
if ($anyFailures -eq $TRUE){
write-host "Failing build as there are broken tests"
$host.SetShouldExit(1)
}

What this does is:

  1. Run Chutzpah from the installed NuGet package location, passing in the location of my Jasmine unit tests. In the case of my project there is a chutzpah.json file in the project which dictates how Chutzpah should run the tests. Also, the JUnit flag is also passed in order that Chutzpah creates a chutzpah-results.xml file of test results in the JUnit format.
  2. We iterate through test results and tell AppVeyor about the the test passes and failures using the Build Worker API.
  3. If there have been any failed tests then we fail the build. If you look here you can see a deliberately failed build which demo's that this works as it should.

That's a wrap - We now have CI which includes our JavaScript tests! That's right we get to see beautiful screens like these:

Thanks to...#

Thanks to Dan Jones, whose comments on this discussion provided a number of useful pointers which moved me in the right direction. And thanks to Feador Fitzner who has generously said AppVeyor will support JUnit in the future which may simplify use of Chutzpah with AppVeyor even further.

Migrating from AngularJS to AngularTS - a walkthrough

It started with nuns. Don't all good stories start that way? One of my (many) aunts is a Poor Clare nun. At some point in the distant past I was cajoled into putting together a simple website for her convent. This post is a walkthrough of how to migrate from AngularJS using JavaScript to AngularJS using TypeScript. It just so happens that the AngularJS app in question is the one that belongs to my mother's sister's convent.

TL;DR - grab what you need#

For reference the complete "before" and "after" projects can be found on GitHub here. This is available so people can see clearly what changes have been made in the migration.

The content of the site is available for reference only

. (Not that I can really imagine people creating their own "Poor Clares" site and hawking it to convents around the globe but I thought I'd make the point.) It looks like this:

Background#

I've been quietly maintaining this website / app for quite a while now. It's a very simple site; 95% of it is static content about the convent. The one piece of actual functionality is a page which allows the user of the website to send a prayer request to the nuns at the convent:

Behind the scenes this sends 2 emails:

  • The first back to the person who submitted the prayer request assuring them that they will be prayed for.
  • The second to the convent telling them the details of what the person would like prayer for.

Right now you are probably thinking this is an unusual post. Perhaps it is, but bear with me.

Over time the website has had many incarnations. It's been table-based layout, it's used Kendo UI, it's used Bootstrap. It's been static HTML, it's been ASP.Net WebForms, it's been ASP.Net MVC and it's currently built using AngularJS with MVC on the back-end to handle bundling / minification and dispatching of emails.

I decided to migrate this AngularJS app to use TypeScript. As I did that I thought I'd document the process for anyone else who might be considering doing something similar. As it happens this is a particularly good candidate for migration as there's a full unit test suite for the app (written with Jasmine). Once I've finished the migration these unit tests should pass, just as they do currently.

You are probably thinking to yourself "but TypeScript is just about adding compile-time annotations right? How could the unit tests not pass after migration?" Fair point, well made. Well that is generally true but I have something slightly different planned when we get to the controllers - you'll see what I mean...

It's also a good candidate for documenting a walkthrough as it's a particularly small and simple Angular app. It consists of just 3 controllers, 2 services and 1 app.

Before I kick off I thought I'd list a couple of guidelines / caveats on this post:

  • I don't intend to say much about the architecture of this application - I want to focus on the migration from JavaScript to TypeScript.
  • The choices that I make for the migration path do not necessarily reflect the "one true way". Rather, they are pragmatic choices that I am making - there may be alternatives approaches here and there that could be used instead.
  • I love Visual Studio - it's my IDE of choice and the one I am using as I perform the migration. Some of the points that I will make are Visual Studio specific - I will try and highlight that when appropriate.

Typings#

The first thing we're going to need to get going are the Angular typing files which can be found on Definitely Typed here. Since these typings are made available over NuGet I'm going to pull them in with a wave of my magic Install-Package angularjs.TypeScript.DefinitelyTyped.

As well as pulling in the typing files Visual Studio 2013 has also made some tweaks to my PoorClaresAngular.csproj file which it tells me about:

And these are the TypeScript specific additions that Visual Studio has made to PoorClaresAngular.csproj:

<Import
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props"
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.Default.props')" />
<TypeScriptToolsVersion>1.0</TypeScriptToolsVersion>
<Import
Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets"
Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />

I'm going to add one extra of my own:

<TypeScriptNoImplicitAny>True</TypeScriptNoImplicitAny>

This prevents you having variables of type any in your TypeScript codebase without you implicitly specifying the type. You can live without this but I've found it's useful to catch where you're missing out on the benefit of static typing. Further to that, this option can be particularly useful when performing a migration. It will become obvious why this is the case as we go on.

I decline the kind opportunity to further search NuGet as I'm already on my way typing-wise. So let's review what has happened. Below you can see the typing files that have been pulled in and that the project and packages files were amended:

Changing JS files to TS files#

This really should be as simple as changing all the JavaScript files underneath the js directory to have the suffix ts. So going from this:

To this:

And if you're not using Visual Studio it is. But if you are using Visual Studio there's a certain amount of fiddling required to include the generated .js and .js.map files associated with each .ts file. The easiest (hah!) thing to do is to crack open the project and wherever you find a &lt;TypeScriptCompile Include="js\somePath.ts" /&gt; to add in 2 Content statements, one for each generated file which states the dependency on the TypeScript file. For example:

<TypeScriptCompile Include="js\services\siteSectionService.ts" />
<Content Include="js\services\siteSectionService.js">
<DependentUpon>siteSectionService.ts</DependentUpon>
</Content>
<Content Include="js\services\siteSectionService.js.map">
<DependentUpon>siteSectionService.ts</DependentUpon>
</Content>

It's a bit of a pain to have to do this at the moment. Hopefully the Visual Studio tooling will catch up so this sort of tweaking becomes unnecessary.

Recap#

So, where are we? Well, we've got our project ready for TypeScript, we've pulled in the Angular typings from Definitely Typed and we've turned all our JavaScript files in the js directory into TypeScript files.

Now we can actually start working through our TypeScript files and ensuring we're all typed correctly. Please note that because I'm working in Visual Studio I get the benefit of implicit referencing; I don't have to explicitly state the typing files each TypeScript file relies on at the head of the file (eg /// &lt;reference path="angularjs/angular.d.ts" /&gt;). If you aren't working in Visual Studio then you'd need to add these yourself.

TypeScriptify app.ts#

Opening up app.ts we're presented with a few red squigglies:

These red squigglies are the direct result of my earlier opting in to NoImplicitAny. So in my view it's already paid for itself as it's telling me where I could start using typings. So to get things working nicely I'll give $routeProvider the type of ng.route.IRouteProvider and I'll explicitly specify the type of any for the 2 params parameters:

// ...
function ($routeProvider: ng.route.IRouteProvider) {
function getTheConventTemplateUrl(params: any) {
var view = params.view || "home";
return "partials/theConvent/" + view + ".html";
}
function getMainTemplateUrl(params: any) {
var view = params.view || "home";
return "partials/main/" + view + ".html";
}
// ...
}
// ...

TypeScriptify siteSectionService.ts#

Opening up siteSectionService.ts we're only presented with a single squiggly, and for the same reason as last time:

This error is easily remedied by giving path the type of string.

What's more interesting / challenging is thinking about how we want to enforce the definition of siteSectionService. Remember, this is a service and as such it will be re-used elsewhere in the application (in both navController and mainController). What we need is an interface that describes what our (revealing module pattern) service exposes:

"use strict";
interface ISiteSectionService {
getSiteSection: () => string;
determineSiteSection: (path: string) => void;
}
angular.module("poorClaresApp.services").factory(
"siteSectionService",
[ // No dependencies at present
function (): ISiteSectionService {
var siteSection = "home";
function getSiteSection() {
return siteSection;
}
function determineSiteSection(path: string) {
var newSiteSection = "home";
if (path.indexOf("/theConvent/") !== -1) {
newSiteSection = "theConvent";
}
else if (path !== "/") {
newSiteSection = "main";
}
siteSection = newSiteSection;
}
return {
getSiteSection: getSiteSection,
determineSiteSection: determineSiteSection
};
}]);

As you can see the ISiteSectionService interface is marked as the return type of the function. This ensures that what we return from the function satisfies that definition. Also, it allows us to re-use that interface elsewhere (as we will do later).

TypeScriptify prayerRequestService.ts#

Opening up prayerRequestService.ts we're again in NoImplicitAny country:

This is fixed up by defining $http as ng.IHttpService and email and prayFor as string.

As with siteSectionService we need to create an interface to define what prayerRequestService exposes. This leaves us with this:

"use strict";
interface IPrayerRequestService {
sendPrayerRequest: (email: string, prayFor: string) => ng.IPromise<{
success: boolean;
text: string;
}>;
}
angular.module("poorClaresApp.services").factory(
"prayerRequestService",
["$http",
function ($http: ng.IHttpService): IPrayerRequestService {
var url = "/PrayerRequest";
function sendPrayerRequest(email: string, prayFor: string) {
var params = { email: email, prayFor: prayFor };
return $http.post(url, params)
.then(function (response) {
return {
success: response.data.success,
text: response.data.text
};
});
}
return {
sendPrayerRequest: sendPrayerRequest
};
}]);

TypeScriptify prayerRequestController.ts#

Opening up prayerRequestController.ts leads me to the conclusion that I have no interesting way left of telling you that we once more need to supply types for our parameters. Let's take it as read that the same will happen on all remaining files as well eh? Hopefully by now it's fairly clear that this option is useful, even if only for a migration. I say this because using it forces you to think about what typings should be applied to your code:

We'll define $scope as ng.IScope, prayerRequestService as IPrayerRequestService (which we created just now) and prayerRequest as { email: string; prayFor: string }. Which leaves me with this:

"use strict";
angular.module("poorClaresApp.controllers").controller(
"PrayerRequestController",
["$scope", "prayerRequestService",
function ($scope: ng.IScope, prayerRequestService: IPrayerRequestService) {
var vm = this;
vm.send = function (prayerRequest: { email: string; prayFor: string }) {
vm.message = {
success: true,
text: "Sending..."
};
prayerRequestService.sendPrayerRequest(prayerRequest.email, prayerRequest.prayFor)
.then(function (response) {
vm.message = {
success: response.success,
text: response.text
};
})
.then(null, function (error) { // IE 8 friendly alias for catch
vm.message = {
success: false,
text: "Sorry your email was not sent"
};
});
}
}]);

I could move on but let's go for bonus points (and now you'll see why the unit test suite is so handy). To quote the Angular documentation:

In Angular, a Controller is a JavaScript constructor function that is used to augment the Angular Scope.

So let's see if we can swap over our vanilla contructor function for a TypeScript class. This will (in my view) better express the intention of the code. To do this I am essentially following the example laid down by my Definitely Typed colleague Basarat. I highly recommend his screencast on the topic. Also kudos to Andrew Davey whose post on the topic also fed into this.

"use strict";
module poorClaresApp.controllers {
class PrayerRequestController {
static $inject = ["$scope", "prayerRequestService"];
constructor(
private $scope: ng.IScope,
private prayerRequestService: IPrayerRequestService) {
}
message: { success: boolean; text: string };
send(prayerRequest: { email: string; prayFor: string }) {
this.message = {
success: true,
text: "Sending..."
};
this.prayerRequestService.sendPrayerRequest(prayerRequest.email, prayerRequest.prayFor)
.then((response) => {
this.message = {
success: response.success,
text: response.text
};
})
.then(null, (error) => { // IE 8 friendly alias for catch
this.message = {
success: false,
text: "Sorry your email was not sent"
};
});
}
}
angular.module("poorClaresApp.controllers")
.controller("PrayerRequestController", PrayerRequestController);
}

My only reservation with this approach is that we have to declare the TypeScript class outside the angular.module... statement. To avoid cluttering up global scope I've placed our class in a module called poorClaresApp.controllers which maps nicely to our Angular module name. It would be nice if I could place the class definition in an IIFE to completely keep this completely isolated but TypeScript doesn't allow for that syntax (for reasons I'm unclear about - the output would be legal JavaScript).

For a small class this seems to add a little noise but as classes grow in complexity I think this approach will quickly start to pay dividends. There are a few things worth noting about the above approach:

  • The required injectable parameters have moved into the class definition in the form of the static $inject statement. I personally like that this no longer sits outside the code it relates to.
  • Because we're using TypeScript arrow functions (which preserve the outer "this" context) we are now free to dispose of the var vm = this; mechanism we're were previously using for the same purpose. Much more intuitive code to my mind.
  • We are not actually using $scope at all in this controller - maybe it should be removed entirely in the long run.

TypeScriptify navController.ts#

navController can be simply converted like so:

"use strict";
interface INavControllerScope extends ng.IScope {
isCollapsed: boolean;
siteSection: string;
}
angular.module("poorClaresApp.controllers").controller(
"NavController",
["$scope", "siteSectionService",
function ($scope: INavControllerScope, siteSectionService: ISiteSectionService) {
$scope.isCollapsed = true;
$scope.siteSection = siteSectionService.getSiteSection();
$scope.$watch(siteSectionService.getSiteSection, function (newValue, oldValue) {
$scope.siteSection = newValue;
});
}]);

I'd draw your attention to the creation of a the INavControllerScope interface that extends the default Angular $scope of ng.IScope with 2 extra properties.

Let's also switch this over to the class based approach (there is less of a reason to on this occasion just looking at the size of the codebase but I'm all about consistency of approach):

"use strict";
module poorClaresApp.controllers {
interface INavControllerScope extends ng.IScope {
isCollapsed: boolean;
siteSection: string;
}
class NavController {
static $inject = ["$scope", "siteSectionService"];
constructor(
private $scope: INavControllerScope,
private siteSectionService: ISiteSectionService) {
$scope.isCollapsed = true;
$scope.siteSection = siteSectionService.getSiteSection();
$scope.$watch(siteSectionService.getSiteSection, function (newValue, oldValue) {
$scope.siteSection = newValue;
});
}
}
angular.module("poorClaresApp.controllers").controller("NavController", NavController);
}

TypeScriptify mainController.ts#

Finally, mainController can be converted as follows:

"use strict";
angular.module("poorClaresApp.controllers").controller(
"MainController",
["$location", "siteSectionService",
function ($location: ng.ILocationService, siteSectionService: ISiteSectionService) {
siteSectionService.determineSiteSection($location.path());
}]);

Again it's just a case of assigning the undeclared types. For completeness lets also switch this over to the class based approach:

"use strict";
module poorClaresApp.controllers {
class MainController {
static $inject = ["$location", "siteSectionService"];
constructor(
private $location: ng.ILocationService,
private siteSectionService: ISiteSectionService) {
siteSectionService.determineSiteSection($location.path());
}
}
angular.module("poorClaresApp.controllers").controller("MainController", MainController);
}

Did it work? Drum roll...#

In unit tests we trust. Let's run them...

Success! I hope you found this useful.