Angular with typescript architecture

Bear with me, this is going to be a long post.

For the past several months I’ve been working on a production single page application written with AngularJS and TypeScript, and I wanted to share how myself and my team have architected the application. In case you were wondering, the app was written using typescript 0.8.3 and not 0.9.1 (which is out now with generics).

In general, I was really unhappy with a lot of the AngularJS examples I had found on the internet when researching how to structure the application, since they all looked flimsy and poorly constructed. While the examples found online were easy to read and follow, they clearly wouldn’t work with an actual large application.

I had several goals:

  • I didn’t want to have to constantly register directives/filters/controllers/etc with Angular everytime I added something
  • I didn’t want to have to update my main index.html page with any references to new files as I worked on them
  • I wanted to avoid string typing as much as possible by centralizing all string references to angular components
  • I wanted everything testable
  • I didn’t want to inline directive html templates, I wanted them in in separate html components
  • I wanted one file for each class
  • I wanted everything strongly typed

Anything less than these requirements, I felt, would compromise maintainability and extensibility for the future, especially if the app had more than one developer working on it.

Folder structure

Starting off, my folder structure looks like this:

app
├──components
├──css
├──img
├──js
    ├──common
    ├──controllers
    ├──data
       ├──locale
    ├──def
       ├──local
       ├──vendor
    ├──directives
    ├──filters
    ├──models
    ├──services
    ├──app.ts
    ├──_all.d.ts
├──locales
├──partials
├──tests
    ├──js
       ├──unitTests
          ├──controllres
          ├──directives
          ├──filters
          ├──models
          ├──services
       ├──e2e tests       
├──index.html
  • components – This is where all the html templates for directives go
  • css – All scss files and the final compiled app.css which is what is linked to from index.html
  • img – All statically served image files
  • js – All typescript files broken down into individually subsections. Also the definitions folder def will be seperated out from any local custom definitions if necessary, and all vendor definitions (such as angular, rxjs, jquery, etc). Also, app.ts (which will be discussed later) is the main app bootloader. _all.d.ts is an aggregate typescript def file that has all the references to the .ts files in the application
  • locales – This is where source .properties files go that can be used to auto generate typescript locale data
  • partials – This is where main angular page entrypoints go. These are the initial partials that are loaded as part of an ng-view change
  • tests – all unit tests, and e2e tests (using angular e2e scenario testing)

Wrapping Angular in OO

There’s no reason you can’t wrap the angularJS object initializers that are used to define directives, filters, controllers, etc, into strongly typed local classes. My team and I built a bunch of templates to use with IntelliJ Idea which made creating new directives/controllers/filters/services/etc extremely easy.

Controllers

Let me demonstrate an example controller:

/// <reference path="../_all.d.ts" /> 

module devshorts.app.controllers {

    import sharedModel = common.model;

    export interface ITest extends ng.IScope{

    }

    export class Test extends ControllerBase {

        public static $inject:string[] = [
            sharedModel.AngularGlobal.$SCOPE
        ];

        constructor(private $scope:ITest) {
            super(arguments, Test.$inject);
        }
    }
}

There are a few things going on here. First, there is a single aggregate definitions file that is always included. This means we don’t need to pollute each file with definitions. The _all.d.ts is also auto generated using a dependency builder we wrote (posted on my github). It finds all .d.ts files based on a dependency configuration and auto populates the all.d.ts for you.

Second, notice that the controller scope is typed with an interface. By making sure we use an interface we can guarantee we won’t try to access a field in the UI that we didn’t explicity expose.

Third, since for controllers I’m using the $inject annotation, we’re making sure to not hardcode any angular strings. Everything is centralized in an AngularGlobal class:

export class AngularGlobal {
        public static $SCOPE = "$scope";
        public static $COOKIE_STORE = "$cookieStore";
        public static NG_COOKIES = "ngCookies";
        ... etc ...
    }

Also, you’ll notice that in the constructor of the controller there is a call to the base class with the inject arguments. The base class is going to validate that the arguments passed to the controller match the items in the $inject array. This way we can get fail fast runtime errors if we ask to inject something, but didn’t wire up the constructor right:

export class ControllerBase {                                                                                                                          
                                                                                                                                                       
    definedArguments(args:any):string[] {                                                                                                              
        var functionText = args.callee.toString();                                                                                                     
        var foundArgs = /\(([^)]+)/.exec(functionText);                                                                                                
        if (foundArgs[1]) {                                                                                                                            
            return foundArgs[1].split(/\s*,\s*/);                                                                                                      
        }                                                                                                                                              
                                                                                                                                                       
        return [];                                                                                                                                     
    };                                                                                                                                                 
                                                                                                                                                       
    constructor(args:any, injection:string[]){                                                                                                         
                                                                                                                                                       
        var expectedInjections = _.zip(this.definedArguments(args), injection);                                                                        
                                                                                                                                                       
        _.each(expectedInjections, val => {                                                                                                            
            var injectionId = val[0];                                                                                                                  
            var argument:string = val[1];                                                                                                              
            if(argument == null){                                                                                                                      
                throw "missing injection id.  Argument for " + injectionId + " is undefined. Make sure to add the ID as part of the $inject function"; 
            }                                                                                                                                          
        })                                                                                                                                             
    }                                                                                                                                                  
                                                                                                                                                      

So, why does moving the constructor to its own class work? Well, look at what angular wants for a controller:

myApp.controller('GreetingCtrl', ['$scope', function($scope) {
    $scope.greeting = 'Hola!';
}]);

And look at what typescript will generate for this class:

var devshorts;
(function (devshorts) {
    (function (app) {
        (function (controllers) {
            var sharedModel = common.model;
            var Test = (function (_super) {
                __extends(Test, _super);
                function Test($scope) {
                    _super.call(this, arguments, Test.$inject);
                    this.$scope = $scope;
                }
                Test.$inject = [
                    sharedModel.AngularGlobal.$SCOPE
                ];
                return Test;
            })(ControllerBase);
            controllers.Test = Test;            
        })(app.controllers || (app.controllers = {}));
        var controllers = app.controllers;
    })(devshorts.app || (devshorts.app = {}));
    var app = devshorts.app;
})(devshorts || (devshorts = {}))

Disregarding all the wrapping for namespaces, you can see that the constructor for Test is just a function that takes a $scope. So, it’s the exact same thing. Now you can wire up your routes in such a way to pair partials with controllers like this:

$routeProvider.when('/test', 
                    this.getRoute(relativePath("partials/test.html"), 
                    devshorts.app.controllers.Test));

Auto registration of services, directives, filters, and models

For our purposes, we did something slightly different with services, models, directives and filters. The reason being that controllers are always “registered” with angular via the routing. However, services, models, directives, and filters are usually registered with separate angular modules that the main app depends on. In this scenario, since it’s common to create lots of directives, etc, we didn’t want to have to manually register anything.

Let me show the model template we have:

/// <reference path="../_all.d.ts" />

module devshorts.app.models {
    'use strict';

    export interface ITestModel {

    }

    export class TestModel implements ITestModel {

        public static ID:string = "TestModel";

        public static injection():any[] {
            return [ AngularGlobal.HTTP, 
                     httpService => new TestModel(httpService) ];
        }
    }
}

Here there is a static ID which is the name of the model, and a static injection function that returns the array notation for injection. In the array notation the last line is the function that gets called by angular with the relevant injectable types. We preferred array notation vs $inject notation since array notation is safe to minimize. The name httpService at this point doesn’t matter, since we asked angular for it using the static AngularGlobal class discussed above.

The ID and injection functions are important, and I’ll show why in a second.

Below is how we’ve bootstrapped angular in the index.html page

$(document).ready(function(){                                        
     var main = new devshorts.app.Main(ipadGlobals.available);

     // when all is done, execute bootstrap angular application      
     angular.bootstrap(document, [NG_GLOBAL.APP_NAME]);              
 });                                                                                                                           

But what is this main class? This is a class that handles all the registration, routing, and other AngularJS setup we need.

                                                                                                                                    
export class Main implements IMain {                                                                                                
                                                                                                                                    
    public app:ng.IModule = angular.module(NG_GLOBAL.APP_NAME,                                                                      
                                            [                                                                                       
                                                NG_GLOBAL.APP_DIRECTIVES,                                                           
                                                NG_GLOBAL.APP_SERVICES,                                                             
                                                NG_GLOBAL.APP_MODELS,                                                               
                                                NG_GLOBAL.APP_PROVIDERS,                                                            
                                                NG_GLOBAL.APP_FILTERS,                                                              
                                                'ui.directives',                                                                    
                                                'ngMobile'                                                                          
                                            ]);                                                                                     
                                                                                                                                    
    public directives:ng.IModule = angular.module(NG_GLOBAL.APP_DIRECTIVES, []);                                                    
                                                                                                                                    
    public services:ng.IModule = angular.module(NG_GLOBAL.APP_SERVICES, []);                                                        
                                                                                                                                    
    public models:ng.IModule = angular.module(NG_GLOBAL.APP_MODELS, [sharedModel.AngularGlobal.NG_COOKIES]);                        
                                                                                                                                    
    public providers:ng.IModule = angular.module(NG_GLOBAL.APP_PROVIDERS,[]);                                                       
                                                                                                                                    
    public filters:ng.IModule = angular.module(NG_GLOBAL.APP_FILTERS, []);                                                          
                                                                                                                                                                                                                                                                     
    constructor(private isAvailable:bool) {                                                                                         
        this.route(this.getApp());                                                                                                  
                                                                                                                                    
        this.wireFactories();                                                                                                       
                                                                                                                                    
        this.configureProviders();                                                                                                  
                                                                                                                                    
        this.configureHttpInterceptors(this.getApp());                                                                              
    }                                                                                                                               
                                                                                                                                    

    ... other methods ...

Again, the app name and other dependency names are all centralized in a static class. The interesting part here is the wireFactories method which looks like this:

wireFactories(){          
    this.wireServices();  
    this.wireDirectives();
    this.wireModels();    
    this.wireFilters();   
}                         

Lets look at wireModels

wireModels(){                                                   
    this.wire(devshorts.app.models, this.models.factory);
}                                                               

And finally looking at wire

/***                                                                                                                 
 * We are simulating doing something like this:                                                                      
 *                                                                                                                   
 *      this.directives.directive(directives.DynamicView.ID, directives.DynamicView.injection());                    
 *                                                                                                                   
 * The "this.directives.directive" is the function we want to call on which is the registration function             
 *                                                                                                                   
 * Since the ID and injection function are statically defined in our classes and MUST be defined for                 
 * our angular injection to work, we can type them temporarily here in this function                                 
 *                                                                                                                   
 * This way if we add new items to any namespace that have an injection function and an ID we will                   
 * automatically register them to the right angular module         *                                                 
 *                                                                                                                   
 * @param namespace                                                                                                  
 * @param registrator                                                                                                
 * @param byPass                                                                                                     
 */                                                                                                                  
  wire(namespace:any, registrator:(string, Function) => ng.IModule , byPass?:(s) => bool){                           
    for(var key in namespace){                                                                                       
        try{                                                                                                         
            if(byPass != null && byPass(key)) {                                                                      
                continue;                                                                                            
            }                                                                                                        
                                                                                                                     
            var injector = <IInjectable>(namespace[key]);                                                            
                                                                                                                     
            if(injector.ID && injector.injection){                                                                   
                registrator(injector.ID, injector.injection());                                                      
            }                                                                                                        
        }                                                                                                            
        catch(ex){                                                                                                   
            console.log(ex);                                                                                         
        }                                                                                                            
    }                                                                                                                                                                                                                                     
}                                                                                                                    

Since namespaces in javascript are just objects with properties, we can make an assumption that anything under the devshorts.app.models namespace is a model if it has a static ID and a static injection function. If it does, then we can register that class with the correct angular module.

Now we never have to worry about wiring up directives, filters, models, or services since at runtime they are auto wired for us. This gives application development a more native feel, just by adding the file means it exists and is available for injection.

Merging the files

I’ve read about people promoting writing everything in javascript into one file, since they don’t want to have the overhead of loading hundreds of .js files on load. I vehemently disagree here. From a development standpoint you should have one class per file. It makes it easier to split up your code, move things around, and to properly organize your application. However, in a production environment you aboslutely should distribute a single merged file. But that’s trivial. I linked to it earlier on, but the dependency tool I had also populates index.html with the appropriate script references for all .js files it finds (that are configured for it to find). If you are interested, go check out the typescript dependency builder (which I will probably blog about again later).

Assuming that your index.html page has all the relevant js files in the head, then it’s easy to write a simple script to merge all the files into one and serve that up when the application is loaded in production. In debug mode, you can serve up all the independent files, it’s just a matter of toggling your node config or your web.config (in an asp.net application).

As an example, here is an F#/C# MSBuild task class to do that for you:

 
public class JsMerger : Task
{
    public string IndexPage { get; set; }
    public List<string> JsFiles { get; set; }
    public String OutputFile { get; set; }

    public override bool Execute()
    {           
        try
        {                
            if (File.Exists(OutputFile))
            {
                File.Delete(OutputFile);
            }

            var filesToProcess = GetFilesToProcess()
                                    .Where(NotOutputOrMinFile)
                                    .Select(f => new JsData
                                    {
                                        FileName = f,
                                        FileContents = File.ReadAllText(f)
                                    });

            using (var output = new StreamWriter(OutputFile))
            {
                foreach (var file in filesToProcess)
                {
                    output.WriteLine("/*!" + Path.GetFileName(file.FileName) + "*/");
                    output.WriteLine(file.FileContents);
                    output.WriteLine(Environment.NewLine);
                }
            }
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            return false;
        }
    }

    private bool NotOutputOrMinFile(string f)
    {
        var path = f;

        var check = OutputFile.Replace(".js", "");

        return (path != check + ".js") && (path != check + ".min.js");
    }

    private IEnumerable<string> GetFilesToProcess()
    {            
        if (!String.IsNullOrEmpty(IndexPage))
        {
            foreach (var jsFile in JsRetriever.getJsFiles(IndexPage))
            {
                yield return jsFile;
            }
        }

        if (JsFiles != null && JsFiles.Count > 0)
        {
            foreach (var f in JsFiles)
            {
                yield return f;
            }
        }
    }
}

Which calls into the following F# script extractor

namespace MergeJsFiles

open HtmlAgilityPack 
open System.Text.RegularExpressions
open System.IO
open System

module JsRetriever =
    
    let stripHtml (text:string) = 
        try   
            let mutable target = text
                     
            let regex = [
                "<script\s*", "";            
                "\"?\s*type\s*=\s*\"\s*text/javascript\s*\"\s*", "";                 
                "</script>", "";
                "src\s*=\s*", ""
                "\"", "";
                ">", "";
                "</",""
                "<",""

            ] 
                
            for (pattern, replacement) in regex do
                    target <- Regex.Replace(target,pattern,replacement).Trim()

            target                 
        with
            | ex -> 
                Console.WriteLine ("Error handling " + text + ", " + ex.ToString())
                ""          

    let convertToAbsolute parent path =
        try            
            Path.Combine(Path.GetDirectoryName(parent), path) |> Path.GetFullPath
        with
            | ex -> 
                Console.WriteLine ("Error handling " + path)
                ""
        

    let endsOn ext file = 
        Path.GetExtension(file) = ext
            
    let getJsFiles (defaultAspxPath:string) = 
        let doc = new HtmlDocument()

        doc.Load defaultAspxPath

        doc.DocumentNode.SelectNodes "/html/head/script/@src" 
            |> Seq.map (fun i -> i.OuterHtml) 
            |> Seq.map stripHtml            
            |> Seq.map (convertToAbsolute defaultAspxPath)
            |> Seq.filter (endsOn ".js")

Conclusion

Organizing an application is hard, and everyone has their own style, but proper application organization and architecture is critical to being able to scale your codebase. Also, sometimes to make the development experience pleasant, you need to invest the time to build tools to automate boring tasks for you. Until we spent the time to create the dependency tools, working with angular and typescript was a real headache, but now it’s an absolute joy.

There are more things I haven’t covered, such as how we dealt with filters, localization, model and service aggregation (for easy injection), and http interceptors but I’ll save those for another post.

25 comments

  1. Oleg

    Next I want to ask is: How can I use dependency injection for this kind of class from Your article
    export class TestModel implements ITestModel {

    public static ID:string = “TestModel”;

    public static injection():any[] {
    return [ () => new TestModel() ];
    }
    }

    How can i inject $http services, for an instance?

      • Oleg

        I guess I get it either. I should use somethink like
        public static injection():any[] {
        return [ ($http) => new TestModel($http) ];
        }
        Am I right?

        • Anton Kropp

          Yes and no. Doing it this way you are relying on naming, so angular knows that $http is the http service. This is a bad idea cause you can’t minimize your code. Instead do something like this (forgive any typos, I’m in tokyo right now typing this on my phone:

          [AngularGlobal.HTTP,
          h => new Test(h)]

          Where AngularGlobal could be a static class and the value HTTP maps the angular id of the http service (the actual value I think is string “$http”). The variable “h” will be given the injected http service. I intentionally named it a single letter to demonstrate that using this method the naming convention no longer matters and is now safe for minimization.

          If you have more questions let me know, I am back next week and will have time to post more detailed examples in gists or on my github.

          • Anton Kropp

            Now it may be clearer what the static “ID” value is on models/directives/services, etc that I defined. You reference the string by the static class so this way you centralize all strings into one location. each model, whatever, will define its own dependencies.

            By defining the dependencies and the final construction function in static properties you can auto wire them up with angular if you follow a specific convention (I used ID and “injection”)

  2. Oleg

    Hello, Anton. I have one more question, not related to this article but to some thing i’m currently working on. May be You know how to register in angular dynamically loaded(via requireJS for an instance) controllers, directives, services, etc?
    Thank You.

  3. Pingback: write angularJS code using typescript | DiscVentionsTech
  4. Oleg

    Hello, Anton. Can You, please, advise me about unit test for controllers / services etc, written using this approach.

  5. Vijay

    Hi Anton,

    Wonderful article, so many things to learn for the guys starting with creating new projects. Please can you share the source code for this sample. It’s going to worth a lot.
    Thanks
    Vijay

    • Anton Kropp

      Richard, this may have changed since I was working with angular. In reality we probably didn’t need to be manually bootstrapping angular at all and could get away with the ngApp directive.

Post a comment

You may use the following HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>