AngularJS and TypeScript

vndr app store image ad

Lately, I’ve started using AngularJS instead of Knockout. Knockout.js is very popular in the .NET world, is very easy to use, and meshes well with .NET devs used to the MVVM style of development. I highly recommend it. But, yeah, it’s fair to say that I’m being a little fickle in deciding to switch. Ask me for a definitive reason and I’ll waffle something about having to create my own binding handlers in knockout. I’m probably just procrastinating.

I’ve also used TypeScript since it was first previewed. I’ve got a couple articles on using Knockout.js with TypeScript, but this time I wanted to get Angular.js working with TypeScript. It turns out, the concepts were even easier to understand once I got it going.

Controllers and Services

When you search for Angular with TypeScript, you’ll inevitably find the link to Piotr Walat’s blog post. It’s a great article and gives a good broad overview, but I wanted to know more specifics of using Angular with TypeScript. I use the popular and very active GitHub project DefinitelyTyped and its angular.d.ts definition, so make sure that’s included in your project before you get going.

I’m going to start with the creation of a service that handles the oh so heavy work of concatenating two strings. Here’s what that TypeScript file looks like:

  
/// <reference path="../../Scripts/angular.d.ts" />

module AngularDemo.Service {

    export class FullNameService {

        constructor () { }

        getFullName(firstName: string, lastName: string) {
            return (firstName || "") + " " + (lastName || "");
        }
    }
}

The service by itself just does some work, but we need to utilize it somewhere. This is done in the Angular (and MVC) concept of the Controller. Here's what my sample controller looks like:

  
/// <reference path="../Model/NameModel.ts" />
/// <reference path="../Service/FullNameService.ts" />
/// <reference path="../../Scripts/angular.d.ts" />

module AngularDemo.Controller {

    export interface IDemoScope extends ng.IScope {
        names: AngularDemo.Model.NameModel;
        getFullName: Function;
    }

    export class DemoController {

        constructor(private $scope: IDemoScope, private fullNameService: AngularDemo.Service.FullNameService, private $location: ng.ILocationService) {
            $scope.names = new AngularDemo.Model.NameModel();
            $scope.getFullName = () => {
                $scope.names.fullName = fullNameService.getFullName($scope.names.firstName, $scope.names.lastName);
                $location.path('/FullName');
            };
        }
    }
}

For starters, look at line 7. Here, there is a new interface IDemoScope that extends the ng.IScope definition provided by the angular.d.ts TypeScript definition file. This lets me to easily reference properties on the Angular $scope object without the TypeScript compiler whining about undefined properties. The IDemoScope is then listed as a parameter on the constructor (line 14). Additionally, I'm passing in a reference to the FullNameService we defined earlier and the standard AngularJS $location service (defined as ng.ILocationService in angular.d.ts).

Then, inside the constructor, the properties on the $scope are filled in with more concrete details.

Primitive Troubles

I ran into an issue that you’ll want to keep an eye on: primitives in the scope with multiple views, which has to do with child scopes when a new view and therefore scope is started. By default, Angular will create new instances of primitive data types in child scopes. This means that if you have a string value in the scope, the new view will have a scope that has a blank value.

The solution is to use a Model. When a model is used, the child scope keeps the reference and doesn’t new up a separate instance. In my example, I use a NameModel that contains the three important properties instead of putting those properties directly on the scope. This ensures that all views will be able to use the same information regardless of the child scope that’s currently active.

Minification

A big issue to lookout for is how your Angular.js code is going to work after it goes through minification. If you’re a .NET MVC programmer, you’ve hopefully seen the built in bundling and minification. If not, let’s take a quick look.

In my project, I've set up the BundleConfig.cs file to refer to the .js files that the TypeScript compiler creates.

  
var angularDemoBundle = new ScriptBundle("~/js/angularDemoScripts");  
angularDemoBundle.Include(  
    "~/App/Service/FullNameService.js",
    "~/App/Model/NameModel.js",
    "~/App/Controller/DemoController.js",
    "~/App/Module/AngularDemoModule.js");

bundles.Add(angularDemoBundle);  

Once you release your code to production where Debug is turned off, the bundler does its thing and minimizes and bundles up your JavaScript. Here’s what our carefully built DemoController constructor looks like after minification (I’ve expanded it so we can easily read it).

  
var AngularDemo;  
(function(n) {
    (function(t) {
        var i = function() {
            function t(t, i, r) {
                this.$scope = t, this.fullNameService = i, this.$location = r, t.names = new n.Model.NameModel, t.getFullName = function() {
                    t.names.fullName = i.getFullName(t.names.firstName, t.names.lastName), r.path("/FullName")
                }
            }
            return t
        }();
        t.DemoController = i
    })(n.Controller || (n.Controller = {}));
    var t = n.Controller
})(AngularDemo || (AngularDemo = {}))

See how the parameter names on line 5 have changed? At this point, Angular has no idea what we were actually trying to inject and throws an error that looks similar to this:

Unknown provider: n from AngularDemo 

The Angular documents warn us about this under "A Note on Minification".

$inject Will Save Us All, Right?

Let’s go back to the DemoController definition and add a static property in, like this:

  
static $inject = ['$scope', 'fullNameService', '$location'];  

Now, when Angular goes through and reads our parameters, it will take the values from our predefined $inject property and use those instead, thereby undoing the effects of the minification parameter renaming.

However, this doesn’t only apply to our constructors, services, and such. It also applies to how we setup our module. This is where I got hung up the most, but it's obvious now that I see the problem. Here's what my module declaration originally looked like: REMEMBER -- THIS BREAKS UNDER MINIFICATION

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

angularModule.config(function ($routeProvider) {  
    $routeProvider.when('/', { templateUrl: './App/Partials/ViewOne.html' }).
        when('/FullName', { templateUrl: './App/Partials/ViewTwo.html' }).
        otherwise({
            redirectTo: '/'
        });
});

angularModule.service('fullNameService', AngularDemo.Service.FullNameService);  
angularModule.controller('AngularDemoController', AngularDemo.Controller.DemoController);  

This follows the same pattern as our original TypeScript controller did, even through it’s straight JavaScript. The minimizer still renames the parameter $routeProvider I declared on line 3. To fix it this time, I’ll use a different pattern to ensure Angular knows what to inject.

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

angularModule.config(['$routeProvider', function ($routeProvider) {  
    $routeProvider.when('/', { templateUrl: './App/Partials/ViewOne.html' }).
        when('/FullName', { templateUrl: './App/Partials/ViewTwo.html' }).
        otherwise({
            redirectTo: '/'
        });
}]);

angularModule.service('fullNameService', AngularDemo.Service.FullNameService);  
angularModule.controller('AngularDemoController', AngularDemo.Controller.DemoController);  

Instead of using a property called $inject in my module declaration, I passed the .config function an array where the first parameter is the name of the constructor arguments. This works in the exact same way and puts everything inline.

A Good TypeScript AngularJS Start

Now we've got some working AngularJS code written in TypeScript and running under a production .NET situation. I'm building my new projects using this pattern and I know it works. Let me know if you've got any more information. As always I've posted the completed source code for this sample on GitHub.

Links

Don't forget to follow me on Twitter @daveiffland