Home AngularJS with Typescript and SystemJS
Post
Cancel

AngularJS with Typescript and SystemJS

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

Walkthrough

This is the application'sstructure:

|-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 ofdefining 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. Theui-bootstrapis used as well, which has dependencies on angular-animate and angular-touch modules.
SystemJSis 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 acommand 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 commandswill install AngularJS, angular-ui-bootstrap, jQuery and node typings and will save them to typings.json.
Wewill revisitthe typings definitions later, when we will addour 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 makethe 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 DefinitelyTypedtypings.
Typings uses the source to determine from which whereit shoulddownload the Typescript definitions.

All the availabletypings 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 moreinfo at the typings website.

tsconfig.json

Next, we should look at tsconfig.json. For this application, I havedefined all theTypescript 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 beexcluded from the compilation process. Iam 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 thesystemjs configuration. We want to tell SystemJS where the essentialpackages for the applicationare,so it can load these in browser.

It is required to specify anentry 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, usingTwitter Bootstrap for styling and the SystemJS library withconfiguration and initialization code to load all of thescript 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, thejson 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 theapp module (the main entry point) can be loaded and essentially bootstrap the application.

app.config.json

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

The configuration file containsthe 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 needto 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 customTypescript definitions.
Into custom directory, we makean 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 toregister 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 theapplication module and then moveto routes.ts.

main.ts

In this file, anapplication module is instantiated, the one that isdeclared 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 noany 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 areloading 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 ared squiggly line? It is because of the node typings we added first in the typings.json.requirecomes fromNodeJS, 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 byangular.ILocationProvider interface and lastly, $routeProvider which is described byangular.route.IRouteProvider interface.
These interfaces are available through DefinitelyTyped Typings. The two first, angular.IModule and angular.ILocationProvidercome 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 ofany, 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 anyis recommended only if you do nothave 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 registeredin the angular module, in main.ts.
I also inject the $location service and $scope. The $locationservicecan be strongly typed by the angular (alias is ng) ILocationService interface.
But we see something strange here. The $scopeis 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 departmentsproperty 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).
Likebefore, 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. Codeis the following:

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

I just declare an interfaceIHomeControllerScope which extendsthe ng.IScope interface. So this one containsthe 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 codein 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.

This post is licensed under CC BY 4.0 by the author.

Angular2 from Scratch - Getting to know Angular2

Mocking a class dependency which casts into a derived type using Moq

Comments powered by Disqus.