AngularJS with Typescript and SystemJS

In this post we are going to talk about AngularJS, Typescript, SystemJS.
Also, we are going to work with typings definitions as well as SystemJS JSON plugin to load JSON configuration in front end code.

Walkthrough

This is the application’s structure:


|-src
|-----main.ts
|-----routes.ts
|-----app.config.json
|-----department.controller
|---------department.controller.html
|---------department.controller.ts
|-----home.controller
|---------home.controller.ts
|---------home.controller.html
|-----department.service
|---------department.service.ts
|-----employee.modal.controller
|---------employee.modal.controller.ts
|---------employee.modal.controller.html
|-typings
|-----custom
|---------appconfig
|-------------index.d.ts
|---------department
|-------------index.d.ts
|---------departmentControllerScope
|-------------index.d.ts
|---------employee
|-------------index.d.ts
|---------employeeModalControllerScope
|-------------index.d.ts
|---------homeControllerScope
|-------------index.d.ts
|---------localStorageService
|-------------index.d.ts
|---------routeParams
|-------------index.d.ts
|-index.html
|-package.json
|-systemjs-config.js
|-tsconfig.json
|-typings.json

We won’t go through every little detail of this application, the link to the Github repository is here and at the end of this article if you want to jump in and take a closer look of the code.
We will go through the aspects of defining an AngularJS application with Typescript, defining our modules with SystemJS, as well as creating custom declarations to support custom structures, like the app.config.json or extend existing declarations.

Now, let’s go through the bits and bolts of the code.

package.json

As you can see, AngularJS 1.5.8 version is going to be used, with angular-route, angular-sanitize and ngstorage additional modules. The ui-bootstrap is used as well, which has dependencies on angular-animate and angular-touch modules.
SystemJS is the module loader and the systemjs-plugin-json is a SystemJS plugin to load JSON, which can be found here.
We add bootstrap as well only for aesthetic purposes.

For devDependencies, lite-server webserver is used to serve content, as well as typescript and typings node modules. Typings module is a Typescript definition manager, which can install and manage Typescript definitions, so Intellisense can be possible in Typescript code.


 "scripts": {
     "start": "concurrently \"npm run tsc\" \"npm run lite\"",
     "postinstall": "typings install",
     "lite": "lite-server",
     "tsc": "tsc -w",
     "test": "echo \"Error: no test specified\" && exit 1"
 },
 "dependencies": {
    "angular": "^1.5.8",
    "angular-route": "^1.5.8",
    "angular-sanitize": "^1.5.8",
    "ngstorage": "^0.3.11",
    "systemjs": "^0.19.37",
    "systemjs-plugin-json": "^0.1.0",
    "angular-animate": "^1.5.8",
    "angular-touch": "^1.5.8",
    "angular-ui-bootstrap": "^2.1.3"
 },
 "devDependencies": {
    "concurrently": "^2.2.0",
    "typescript": "^1.8.10",
    "lite-server": "^2.2.2",
    "typings": "^1.3.3"
 },

 

typings.json

This file contains all the global dependencies to Typescript definitions for AngularJS, angular-ui-bootstrap, node, jQuery  as well as custom definitions, which we are about to build in a few minutes.

So, we need Typescript definitions for our code, in order to have Intellisense.
To add Typescript definitions for frameworks/libraries you use the typings.json file, which is a configuration file, and then a command prompt window to install these definitions.
Below are some examples of adding definitions for angular, angular-ui-bootstrap, jquery and node.

typings install --global --save dt~angular

typings install --global --save dt~angular-ui-bootstrap

typings install --global --save dt~jquery

typings install --global --save dt~node

These commands will install AngularJS, angular-ui-bootstrap, jQuery and node typings and will save them to typings.json.
We will revisit the typings definitions later, when we will add our custom ones for the app.config.json.
These commands from above will create a typings directory in application root, which will contain *.d.ts files.

The –global flag is used to make the definition global to the application. It will be referenced in the index.d.ts under typings directory. This will be used from all the Typescript files automatically.
The –save flag is used to save the definition to our typings.json file.
You probably have noticed that in front of the framework/library name we put something like “dt~”. This is the source option selection. The dt brings the DefinitelyTyped typings.
Typings uses the source to determine from which where it should download the Typescript definitions.

All the available typings sources (as from Typings Github page):

  • npm – dependencies from NPM
  • github – dependencies directly from GitHub (E.g. Duo, JSPM)
  • bitbucket – dependencies directly from Bitbucket
  • bower – dependencies from Bower
  • common – “standard” libraries without a known “source”
  • shared – shared library functionality
  • lib – shared environment functionality (mirror of shared) (–global)
  • env – environments (E.g. atom, electron) (–global)
  • global – global (window.<var>) libraries (–global)
  • dt – typings from DefinitelyTyped (usually –global)

If you want to find a definition but you are not sure about its metadata, you can search for it in the registry.

You get all the /*angular*/ definitions, with detailed information by:
typings search angular

Or by name:

typings search --name angular

You can find more info at the typings website.

tsconfig.json

Next, we should look at tsconfig.json. For this application, I have defined all the Typescript files to be included in the compilation process in the files array property, being explicit about them.


{
    "compilerOptions": {
        "module": "commonjs",
        "moduleResolution": "node",
        "noImplicitAny": false,
        "sourceMap": true,
        "target": "es6"
    },
    "files": [
        "typings/index.d.ts",
        "src/routes.ts",
        "src/main.ts",
        "src/department.service/department.service.ts",
        "src/department.controller/department.controller.ts",
        "src/home.controller/home.controller.ts"
    ],
    "exclude": [
        "node_modules",
        "typings"
    ]
}

The files instruction, specifies the files that will be included by the compiler. Any other file not included in files array will be excluded from the compilation process. I am explicit in defining all the visible files to Typescript compiler, because I want certain of them to be compiled. And notice, I include the typings/index.d.ts as well, because I want it to be visible, essentially omitting all the other typings definitions, as they are redundant. Typescript only needs to know about the index.d.ts and it will follow the /// <reference /> delarations when it is compiling.
The exclude array contains all the paths that should be excluded from typescript compiler.

systemjs-config.js

Next, let’s visit the systemjs configuration. We want to tell SystemJS where the essential packages for the application are, so it can load these in browser.

It is required to specify an entry point, which will be the main.js script, as well as additional packages which are used throughout the application like angular, angular-sanitize, angular-route, ngstorage, ngAnimate, ngTouch, angular-ui-bootstrap and the systemjs-plugin-json plugin.

The code of systemjs-config.js is the following:


(function () {
    var map = {
        "app": "src",
        "angular": "node_modules/angular",
        "ngSanitize": "node_modules/angular-sanitize/",
        "ngRoute": "node_modules/angular-route/",
        "ngStorage": "node_modules/ngstorage/",
        "ngAnimate": "node_modules/angular-animate/",
        "ngTouch": "node_modules/angular-touch/",
        "angular-ui-bootstrap": "node_modules/angular-ui-bootstrap/",
        "json": "node_modules/systemjs-plugin-json/"
    };

    var packages = {
        "app": { main: "main.js", defaultExtension: "js" },
        "angular": { main: "index.js", defaultExtension: "js" },
        "ngSanitize": { main: "index.js", defaultExtension: "js" },
        "ngRoute": { main: "index.js", defaultExtension: "js" },
        "ngAnimate": { main: "index.js", defaultExtension: "js" },
        "ngTouch": { main: "index.js", defaultExtension: "js" },
        "angular-ui-bootstrap": { main: "index.js", defaultExtension: "js" },
        "ngStorage": { main: "ngStorage.js", defaultExtension: "js" },
        "json": { main: "json.js", defaultExtension: "js" }
    };

    var config = {
        map: map,
        packages: packages
    };
    System.config(config);
})();

index.html

This a simple HTML file, using Twitter Bootstrap for styling and the SystemJS library with configuration and initialization code to load all of the script dependencies.

The markup should be like this:


<!DOCTYPE html>
<html lang="en" ng-app="app">

<head>
 <base href="/" />
 <title>AngularJS + Typescript + SystemJS</title>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <link href="/node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet">

 <!--Module loader-->
 <script src="/node_modules/systemjs/dist/system.src.js"></script>
 <script src="/systemjs-config.js"></script>
 <script>
     System.import("json").then(function() {
          return System.import("app").catch(function(err) {                 console.log(err); });
     }).then(null, function(err){
          console.error("Oh no, error!", err);
     });
 </script>
</head>

<body>
     <div class="container">
         <ng-view></ng-view>
     </div>
</body>

</html>

It is pretty straight-forward what is going on here.
For SystemJS initialization with the json plugin, the json plugin is imported first, by calling System.import("json"). This will load the plugin and will return a promise. When the plugin is successfully loaded, then the app module (the main entry point) can be loaded and essentially bootstrap the application.

app.config.json

This file is under the src/ directory. All the client configuration settings are defined here. Application settings are about the routes path, templateUrl, controller names, as well as modal configuration. By using this JSON file, the source code becomes cleaner, configurable, avoiding magic strings/numbers/etc.

The configuration file contains the following code:

{
    "client": {
        "basePath": "/",
        "html5Mode": true,
        "routes": [
            {
                "path": "/",
                "templateUrl": "src/home.controller/home.controller.html",
                "controller": "homeController"
            },
            {
                "path": "/department/:id",
                "templateUrl": "src/department.controller/department.controller.html",
                "controller": "departmentController"
            }
        ],
        "departments": [
            {
                "name": "sales",
                "maxAllowedEmployees": 4
            },
            {
                "name": "it",
                "maxAllowedEmployees": 10
            }
        ],
        "modal": {
            "animation": true,
            "size": "lg",
            "templateUrl": "src/employee.modal.controller/employee.modal.controller.html",
            "controller": "employeeModalController",
            "controllerAs": "$scope"
        }
    }
}

All these are great, but we want a way to make this structure available through Intellisense for our Typescript code.

To achieve this, we need to create a *.d.ts file, declaring all this configuration.

To do so, we go to typings directory and create a custom directory there. This will hold all custom Typescript definitions.
Into custom directory, we make an appconfig directory and inside that an appconfig.d.ts file. Code of this file:


declare module AppConfig {
    interface Route {
        path: string;
        templateUrl: string; 
        controller: string;
    }

    interface Department {
        name: string;
        maxAllowedEmployees: number;
    }
    
    interface Modal {
        animation: boolean;
        size: string;
        ariaLabelledBy: string;
        ariaDescribedBy: string;
        templateUrl: string;
        controller: string;
        controllerAs: string;
    }

    interface Client {
        basePath: string;
        html5Mode: boolean,
        routes: Route[];
        departments: Department[],
        modal: Modal
    }

    export interface Configuration {
        client: Client;
    }
}

In the *.dt.s file, a module is declared, with all the configuration exported as an interface.

Now, go back to command prompt and type the following:


typings install --global --save file:typings/custom/appconfig/appconfig.d.ts

This will add the appconfig.d.ts in the typings.json as well as a /// <reference /> in the index.d.ts, so it will be available globally throughout the application.

The syntax to register globally a custom definition, located in disk:


typings install --global--save file:path/to/the/.d.ts

Good, app.config.json and its typings are all set.
It’s time to investigate the application module and then move to routes.ts.

main.ts

In this file, an application module is instantiated, the one that is declared in the ng-app directive of the index.html file.

import * as angular from "angular";
import "ngSanitize";
import "ngRoute";
import "ngStorage";
import "ngAnimate";
import "ngTouch";
import "angular-ui-bootstrap";
import { HomeController } from "./home.controller/home.controller";
import { DepartmentController } from "./department.controller/department.controller";
import { DepartmentService } from "./department.service/department.service";
import { EmployeeModalController } from "./employee.modal.controller/employee.modal.controller";
import { registerRoutesFor } from "./routes";

export module app {
    "use strict";
    var app = angular.module("app", ["ngSanitize", "ngRoute", "ngStorage", "ngAnimate", "ngTouch", "ui.bootstrap"])
                .controller("homeController", HomeController)
                .controller("departmentController", DepartmentController)
                .controller("employeeModalController", EmployeeModalController)
                .factory("departmentService", [DepartmentService]);
    
    registerRoutesFor(app);

    export var angularModule = app;
}

Essentially, the “app” module is instantiated, registering all the additional modules, controllers and services to it. It is not necessary to export it, except you need it in other files. In this particular application it is not needed for anything else, as it served its purpose in the main.ts file.

routes.ts

In this file, a function is exported, which registers all the available routes for the application. Notice the use of the .json configuration file.


var config: AppConfig.Configuration = require("./app.config.json!");
import "angular";

export function registerRoutesFor(app: angular.IModule) {
    "use strict";

    app.config(($routeProvider: angular.route.IRouteProvider, $locationProvider: angular.ILocationProvider) => {
        $locationProvider.html5Mode(config.client.html5Mode);

        let home = config.client.routes.find(v => v.controller === "homeController");
        let department = config.client.routes.find(v => v.controller === "departmentController");

        $routeProvider
            .when(home.path, {
                templateUrl: home.templateUrl,
                controller: home.controller
            })
            .when(department.path, {
                templateUrl: department.templateUrl,
                controller: department.controller
            })
            .otherwise({
                redirectTo: config.client.basePath
            });
    });
}

You should notice no any string literals exist, except the ones to find the correct route for each controller. Aside from that, everything is configurable through our app.config.json. Just to point out though, if we didn’t use the systemjs-plugin-json the code above wouldn’t be able to compile. This is because SystemJS cannot find the json file. It is not configured to do so, so it cannot be served.
Notice, to fetch the .json file we use var and the require method. We are loading a JSON file, not a module, so import shouldn’t be used is this case. When var is used, require() is treated like a normal function.
You should be wondering, how require function is recognized by Typescript and it is not over a red squiggly line? It is because of the node typings we added first in the typings.json. requirecomes from NodeJS, so we needed node Typescript definitions to have this without the compiler complaining.

Also, notice that parameters, as well as variables are strongly typed, like angular app module, which is described by the angular.IModule interface, $locationProvider which is described by angular.ILocationProvider interface and lastly, $routeProvider which is described by angular.route.IRouteProvider interface.
These interfaces are available through DefinitelyTyped Typings. The two first, angular.IModule and angular.ILocationProvider come from the angular definitions, while the latter, angular.route.IRouteProvider comes from the angular-route typings definitions. These are installed through typings install command.
We prefer using definitely typed code., instead of any, as it makes the code more verbose for us developers. We don’t need to know the API by heart, that’s why we are using tools like VSCode, Visual Studio, etc, or Typescript, which assist in the development process.
Usage of any is recommended only if you do not have the typings or you are quickly casting your object, in order to avoid compile-time errors.

Let’s now visit some of the controllers in application, to see how we can handle strongly typed parameters like angular $scope.
We need to identify in which definition each parameter exists. For example, $scope or $location service are included in angular definition. So we need to install angular typings.
Others, like $uibModal are defined in angular-ui-bootstrap typings, so we need to download this from the typings registry.

Now, let’s see the home controller. HomeController’s job is to display all departments, which are fetched from a service, stored in an angular $scope property and displayed in the template. Each department has a unique integer Id, which is used to navigate to a new route /route/:id, through a method called navigateToDepartment.


import { DepartmentService } from "../department.service/department.service";

export class HomeController {
    constructor(
        private departmentService: DepartmentService,
        private $scope: IHomeControllerScope,
        private $location: ng.ILocationService) {
        $scope.departments = departmentService.getDepartments();
        $scope.navigateToDepartment = this.navigateToDepartment;
    }

    navigateToDepartment = (id: number) => {
        this.$location.path(`/department/${id}`);
    }
}

As you can see in code above, I create a new class named HomeController. In its constructor, I define the service I want to inject, which is the DepartmentService imported few lines before. This service is registered in the angular module, in main.ts.
I also inject the $location service and $scope. The $location service can be strongly typed by the angular (alias is ng) ILocationService interface.
But we see something strange here. The $scope is of type IHomeController, which is something not included in the angular typings definition. In fact, this is something custom I created, in order to define the departments property and the navigateToDepartment method. If I used the ng.IScope, I may had $scope strongly typed, but Typescript compiler would give me an error, because it can’t recognize those two (departments and navigateToDepartment).
Like before, with the custom appconfig.d.ts, I create a Typescript declaration file. In typings/custom directory I create a folder named homeControllerScope, which includes an index.d.ts file. Code is the following:


declare interface IHomeControllerScope extends ng.IScope {
    departments: Department[];
    navigateToDepartment: (id: number) => void;
}

I just declare an interface IHomeControllerScope which extends the ng.IScope interface. So this one contains the contract of IScope as well its custom API. And if you are wondering, what is this Department type, it is just another custom declaration file.
I definitely recommend, when creating controller classes, to define an interface as well for the $scope object of the controller, a custom one, containing all your properties, functions, etc.

Take a look at the code in this Github repository. Clone the repository in your local machine, run the code and then have a tour. Check the typings.json configuration, as well as the controllers, how they are defined and used. Try to extend the application, by creating an edit controller for the users added in each department. Make your code strongly typed and if needed create your own declaration files.

Advertisements

7 thoughts on “AngularJS with Typescript and SystemJS

  1. Good sir! I have to tell you that this it the only post that I have been able to find that actually gave me helpful information on how to install a custom “.d.ts” file in my Angular 2 app (using System.js). I’ve been Googling for weeks and have come up empty handed until I saw your “files”:[] example and the way to run the install. Thank you so much!

    Liked by 1 person

  2. Like Ben, this has been a long road of searching for clear examples of this, so thank you for taking the time to write this.

    Unfortunately for me, my struggle continues since there are no examples of how to continue using protocol buffers in TypeScript so far as I can tell despite there being several tools posted to NPM, etc. For example, you can convert .proto files to JSON using the protobufjs library’s pbjs executable, then use proto2ts to create typings definitions for those JSON files. However no amount of including these into my typings definition has allowed me to import the modules or interfaces defined in those typings files into my typescript files. I always get unresolved name or other errors, and the ‘require’ keyword isn’t allowed in ES2015 modules.

    Any chance you’ve encountered this?

    Liked by 1 person

      1. Thank you for the response. I’ve managed to find a go-path, but it has left me even more confused.

        The first issue I ran into is that the proto2ts utility is no longer maintained and does not support message extensions (ProtoBuf version 2’s analog to extending an interface or subclassing), which we were using. The output file from that utility was invalid syntax, hence many of my errors. I’ve since corrected that by being “less fancy” with our message definitions, according to a coworker, so now the contents of the descriptor .d.ts has correct syntax at the expense of duplicating 9 of 10 fields in every message type we have. …The sacrifices we make… LOL

        I’ve tried setting up a plunkr to show what I’ve tried, but it’s failing to resolve the imported relative pathname to the descriptor filename like TypeScript’s compiler would do with “node” module resolution set. It also fails to find the module name globally if I use the triple-slash reference method: /// .

        For the sake of it, the module definition output by proto2ts is essentially things like this:

        // somename.d.ts
        declare module somename {
        export interface Something { }
        export interface SomethingMessage extends Something { }
        export interface SomethingBuilder { }
        }

        I’m sure I’m doing something silly on the import/access of its modules. So far the only way I can get things to work is to move the descriptor out of my TS source tree and add a “/// ” so that the compiler pulls it back in ad-hoc. Then TypeScript seems okay with the module declaration and I can access the module names within the file.

        The various alternatives that do not work are: including the descriptor in the main source tree and transpiling it with everything else, trying to import the module name from the file with a relative import, or including the file in my typings (and then including typings/index.d.ts in my TypeScript config), etc.

        As I said at the start of this message, I do have a go-path at this point, so that’s something. However if you have some thoughts on what I’m misunderstanding in that last paragraph I would appreciate the education. I’m a bit new still to TypeScript.

        Liked by 1 person

      2. Actually I spoke a little too soon. While my library can now transpile fine, the triple-slash references in the files consuming the .d.ts end up with updated URLs on the output of the transpiler. So for example, in the source file (call it something.ts) I have it configured: /// . The output of transpiling, the something.d.ts updated the path to: /// which breaks because I do an “unpack” step on post-install of my library. So for any consumers of my library, the path should be “../../../special.d.ts”. So far I haven’t found anything in the tsconfig spec to show how to disable that behavior.

        Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s