Click here to Skip to main content
15,867,568 members
Articles / Web Development

Angular 1.4.8 Strongly Typed in TypeScript

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
2 Jan 2017CPOL19 min read 21.1K   166   3   2
This article shows how to write strongly typed client-side scripts in AngularJs framework with TypeScript.

Introduction

Angular JS is currently the most common client-side framework to build responsive, fast and testable web applications. It allows to write complex code with respect to MVC (Model-View-Controller) pattern. There are many add-ons and libraries already written to Angular Js and the DI (Dependency Injection) pattern allows quick integration with these libraries, so making the application is very handy.

Typescript is a language formed by Microsoft and it is used to write strongly-typed code that compiles to JavaScript (EcmaScript 5) as an output. It introduces many ideas taken from fully modern Object-Oriented languages like Java or C#. Thanks to compiling process, result code can be successfully used in all modern browsers.

Background

Recently, I had to write an application in Angular Js on the client-side.
As I mostly code on the back-end side, I used to write strongly-typed code and I am not keen on writing code in pure JavaScript.

I knew there is such a thing as TypeScript. I already used it, but in common with the Knockout Js framework.

The main question was: how to integrate Angular Js and Typescript, to write nice and good quality code I use to write on the back-end side ?

There are many tutorials on how to use Angular Js. There is also much documentation about Typescript. But it is hard to find out how to mix it.

Finally, I did it and I think results are not so bad, so I want to briefly share my experience.

Using the Code

Environment Setup

First of all, to start the journey, these applications need to be installed:

There are many tutorials on how to properly configure Node environment, so I will not cover it here. Hope you pass through it and the "npm" command is available in the Command Line, as it would be used a little bit here.

After the install, in the project folder (e.g. C:/Projects/NewsApp), run the npm init command and follow the instructions (set application name, version, description, author, etc.):

npm init

After the command is executed, a package.json is created. It defines the application and its dependencies. Actually, it is a bit short, but while the project gets more complicated, the file grows.

Now it is time to install bower (https://bower.io), a package manager for JavaScript libraries:

npm install bower -g

Good Practice

Currently, npm allows installing also client-side libraries such as JQuery or Angular JS. Many people prefer only usage of npm, but I think it is a good practice to separate the client-side and the server-side libraries and use accordingly the bower and the npm. It is up to the developer though, whether to use Bower or not. :)

Now, bower packages can be initialized by typing in the command line:

bower init

After specifying project properties, a bower.json file should be created.

Thanks to package.json and bower.json files, all packages required by the application are not required to be stored in the remote repository. Recovering those packages is as simple as executing the below commands in command line.

npm install
bower install

Executing these actions would restore all packages defined in the package.json and bower.json (please note that packages installed globally, such as bower, would not be restored).

In order to have ready environment to work, Angular has to be installed.

So, execute in the Command Line:

bower install angular@1.4.8 --save

The --save property would save the angular dependency in the bower.json file.

Another necessary action is to install the Typings manager (https://github.com/typings/typings). In command line, execute:

npm install typings --global

Now we have access to typings CLI. To check if the typings exists, execute such command:

typings search angular

As there are many libraries connected with Angular, it is possible to filter the results by exact name.

typings search --name angular

Finally, there is typings package for Angular. Installing this package should be very simple, just executing typings install angular? Unfortunately, no. :(

Because angular typings are in the old repository, it is necessary to add source to the path, so will it be something like this: typings install dt~angular? Unfortunately, no. :(

In this article, we use Angular version 1.4.8. So we have to fix the command, will typings install dt~angular@1.4 work? Still no. This version is deprecated, so we have to add --global to get this working.

Finally, the working command is:

typings install dt~angular@1.4 --global

Now in the NewsApp folder, there is a "typings" folder created and inside it, there is an Angular folder with index.d.t.s file. In this file, all necessary definitions are.

That's it! All (of course not all, but for this moment) necessary components are installed, let's kick off! :)

Hello World from Angular Application

Now, let's get to coding.

At the beginning, let's add index.html file with some basic HTML tags.

HTML
<!DOCTYPE html>
<html>
    <head>
        <meta name="description" content="News portal example with Angular JS.">
        <title>
            News Portal greets you!
        </title>
    </head>
    <body>
        <header>
            <h1>
                Hello world!
            </h1>
        </header>
    </body>
</html>

The site is working, but it is nothing special. So let's add the JavaScript part, starting with app.js file.

JavaScript
var app = angular.module("NewsApp", []);

app.controller("NewsController", ["$scope", function($scope){
    $scope.helloWorld = "Hello from AngularJS world!";
}]);

And update the index.html file:

HTML
<!DOCTYPE html>
<html>
    <head>
        <meta name="description" content="News portal example with Angular JS.">
        <title>
            News Portal greets you!
        </title>
    </head>
    <body ng-app="NewsApp">
        <section ng-controller="NewsController">
            <header>
                <h1 ng-bind="helloWorld"></h1>
            </header>
        </section>
    </body>
    <script type="text/javascript" src="app.js"></script>
</html>

Ok, nothing happened, Hello World is not displayed. In the console, there is an error: "angular is not defined".. Yea, we have not referenced the angular sources. Before app.js script, add reference to angular:

JavaScript
<script type="text/javascript" src="bower_components/angular/angular.js"></script>

Yay, it works ! :)

But it starts like a billion AngularJs tutorials.

Let's make it more...

Definitely Typed AngularJs

First of all, let's rename our app.js to app.ts. Now IDE (Visual Studio Code at least) underscores the angular keyword. To fix it, let's add reference to angular definitions. I used to do it with...

JavaScript
/// <reference path="typings/index.d.ts" />

...as the first line in the file (as in the server-side languages such as C# and Java).

Now the code is valid as nothing gets underscored. Hovering mouse at angular or controller keywords show types of the components. We can even navigate to these components by hitting F12 when the caret is at the specific component (exactly as in the Visual Studio when working with server-side applications).

So let's do some cleanup. In the project folder (e.g. NewsApp), let's create scripts and dist directories.
In scripts directory, let's create controllers folder.

Now let's create the newsController.ts file and move here the NewsController implementation:

JavaScript
function newsController($scope){
    $scope.helloWorld = "Hello from AngularJS world!";
};

Let's move the app.ts file to scripts directory and leave only registration of the controller:

JavaScript
/// <reference path="typings/index.d.ts" />

var app = angular.module("NewsApp", []);
app.controller("NewsController", ["$scope", newsController]);

Good, but not the best.

Let's make NewsController more definitely typed and make app.ts not know about any NewsController's dependencies (Open-Closed principle in SOLID).

After changes, app.ts looks very simple:

JavaScript
/// <reference path="../typings/index.d.ts" />
/// <reference path="controllers/newsController.ts" />

var app = angular.module("NewsApp", []);
app.controller("NewsController", NewsApp.NewsController);

The NewsController implementation looks more like strongly typed code every backend developer used to:

JavaScript
module NewsApp
{
    export class NewsController
    {
        public static $inject = ["$scope"];

        constructor($scope: any){
            $scope.helloWorld = "Hello from AngularJS world!";
        }
    }
}

The only thing that is a bit odd is the public static field $inject. Unfortunately, it is necessary for Angular to resolve dependencies and to register controller by the controller's constructor. In the $inject field, all dependencies injected to controller are specified.
There are some other substitute implementations of $inject field (for example, in comment), but I am used to this one. :)

In the example above, there is also a module keyword added. It is a good practice to declare classes in specific modules as it cleans up the code.

Everything is fine, the code is not underscored, so it might work. It might, but now the application is not working as the code is written in the typescript language and browsers can't compile it.

Now is the time to install tool that could compile it. In this example, let's use gulp as it is pretty simple and highly configurable. There are other tools, such as grunt or webpack, but for this example, gulp is fine. :)

In command line, execute:

npm install gulp --save

This installs gulp tool as the project dependency. To run gulp, add gulpfile.js in the project directory, with some example to check if it works:

JavaScript
var gulp = require('gulp');

gulp.task('default', function(){
    console.log('Works!');
});

In the command line type simple:

gulp

If the "works!" is displayed in the console, then it works. :)

Now to get it actually doing something, let's add other dependencies:

npm install gulp-typescript --save
npm install gulp-concat --save
npm install gulp-sourcemaps --save
npm install typescript --save

Let's change the gulp file to compile typescript files and create output file sources.js:

JavaScript
var gulp = require('gulp');
var ts = require('gulp-typescript');

gulp.task('typescript', function(){
    return gulp.src('scripts/**/*.ts')
        .pipe(ts({
            declaration: false,
            out: 'sources.js',
            target: 'ES5'
        }))
        .pipe(gulp.dest('dist'));
})

gulp.task('watch', function(){
    gulp.watch('scripts/**/*.ts', ['typescript']);
})

gulp.task('default', ['typescript', 'watch']);

Now running...

gulp

...command in the Command Line should compile typescript files and create JavaScript file structure in the dist folder. Additionally, there is a sources.js file which has all files concatenated inside it.

Another benefit of using typescript and "references" in files is correct file structure. That means all dependant files are compiled and declared before types that use them. Magic, isn't it? :)

Now, let's update the index.html file:

JavaScript
<script type="text/javascript" src="bower_components/angular/angular.js"></script>
<script type="text/javascript" src="dist/sources.js"></script>

And the application is working! :)

Good Practice

It is good practice to automate as much as can be done in gulp. So in this case, a gulpfile can be updated to add sourcemaps for all our sources (typescript files) and all libraries could be concatenated in a single file, so the index.html file will not have to be updated (there is also a performance benefit when including all sources in one file).

After the changes, gulpfile looks like this:

JavaScript
var gulp = require('gulp');
var ts = require('gulp-typescript');
var sourcemaps = require('gulp-sourcemaps');
var concat = require('gulp-concat');
var lib = require('bower-files')();

gulp.task('typescript', function(){
    return gulp.src('scripts/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts({
            declaration: false,
            out: 'sources.js',
            target: 'ES5'
        }))
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'));
});

gulp.task('libs', function(){
    return gulp.src(lib.ext('js').files)
        .pipe(concat('libs.js'))
        .pipe(gulp.dest('dist'));
})

gulp.task('watch', function(){
    gulp.watch('scripts/**/*.ts', ['typescript']);
})

gulp.task('default', ['libs', 'typescript', 'watch']);

And now, the last small change in the index.html file:

JavaScript
<script type="text/javascript" src="dist/libs.js"></script>
<script type="text/javascript" src="dist/sources.js"></script>

Services, Directives and More

Ok, so the environment is ready to go. Let's improve the application by adding NewsService that gets news from the WebApi.

To do that, create directory "services" in the scripts directory. Inside services directory, add newsService.ts file:

JavaScript
/// <reference path="../../typings/index.d.ts" />

module NewsModule{
    export class NewsService{
        public static $inject = ['$http'];

        constructor(private $http: ng.IHttpService) {
        }

        public GetAllNewses(): ng.IHttpPromise<string>{
            return this.$http.get("https://jsonplaceholder.typicode.com/posts");
        }
    }
}

The NewsService gets $http service injected into constructor and exactly as the controller, it has the public static $inject field with defined dependency. In this example, I would use the jsonplaceholder site to get some stubbed posts and display them as News.
The private keyword in the constructor's parameter $http defines the variable as the class field, so there is no need for explicit initialization.

To get all strongly typed, $http service is defined as ng.IHttpService from angular typings (reference to typings/index.d.ts).

In the app.ts file, NewsService has to be registered:

JavaScript
app.service("NewsService", NewsModule.NewsService);

If the IDE is not recognizing NewsService type, a reference to the file has to be added above.

Ok, gulp is working fine, everything compiles. But the result of a GET request is a JSON, and the result of the GetAllNewses method is a promise of string. It doesn't look good. Let's add NewsModel and return strongly typed result from service.

In the scripts directory, add models directory and inside, add newsModel.ts file.

JavaScript
module NewsModule{
    export class NewsModel{
        public id: number;
        public title: string;
        public body: string;
    }
}

For now, this model is ok.

Now let's change the GetAllNewses method to use the defined model:

JavaScript
public GetAllNewses(): ng.IHttpPromise<NewsModel[]>{
    return this.$http.get<NewsModel[]>("https://jsonplaceholder.typicode.com/posts");
}

With such implementation, we can use the NewsService in the NewsController.

JavaScript
module NewsModule {
    export class NewsController {
        public static $inject = ["$scope", "NewsService"];

        constructor(private $scope: any, private newsService: NewsService){
            $scope.helloWorld = "Hello from AngularJS world!";
            this.LoadNewses();
        }

        private LoadNewses(){
            this.newsService.GetAllNewses()
                .then(success => {
                    this.$scope.newsList = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

To see the results in the application, index.html requires little modification:

HTML
<body ng-app="NewsApp">
    <section ng-controller="NewsController">
        <header>
            <h1 ng-bind="helloWorld"></h1>
        </header>
        <ul>
            <li ng-repeat="news in newsList">
                <span ng-bind="news.title"></span>
            </li>
        </ul>
    </section>
</body>

Yay so many results... :) Small amendments in code and we have displayed last twenty News. :)

HTML
<li ng-repeat="news in newsList | orderBy: '-id' | limitTo: 20">
    <div ng-if="$index === 0">
        <h3 ng-bind="news.title"></h3>
        <p ng-bind="news.body"></p>
    </div>
    <span ng-if="$index > 0" ng-bind="news.title"></span>
</li>

OK, but the NewsController is a bit ugly. It uses $scope of type any.. Let's fix it.

Angular allows controller registration in view with syntax: Controller AS nameController.

Thanks to that, the Controller is $scope and registering anything in the Controller as its property/field or method makes it visible in the view.

Let's change the view first:

HTML
<section ng-controller="NewsController as News">
    <header>
        <h1 ng-bind="News.helloWorld"></h1>
    </header>
    <ul>
        <li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
            <div ng-if="$index === 0">
                <h3 ng-bind="news.title"></h3>
                <p ng-bind="news.body"></p>
            </div>
            <span ng-if="$index > 0" ng-bind="news.title"></span>
        </li>
    </ul>
</section>

And now the NewsController:

JavaScript
module NewsModule {
    export class NewsController {
        public static $inject = ["NewsService"];

        public helloWorld: string;
        public newsList: NewsModel[];

        constructor(private newsService: NewsService){
            this.helloWorld = "Hello from AngularJS world!";
            this.LoadNewses();
        }

        private LoadNewses(){
            this.newsService.GetAllNewses()
                .then(success => {
                    this.newsList = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

As you can see, $scope is completely removed. Instead, all properties are defined in NewsController class and are accessible with the this keyword.

Now let's make a directive. As an example, we can make a directive that returns Full User Name for given UserId. I know that in real life, there should be some UserService that would get User Details for given UserName, but for this example, we can just stub user names.

Think of directives as reusable components. Not like "I have ng-repeat and let's do directive" but "I need to use it within few different views". One of my favourite usages of directives is the transformation of Enum types to display string. FullUserName directive is similar example, but instead of the Enum type, it is based on userId.

In the scripts directory, create directives directory and inside, add file fullUserNameDirective.ts.

JavaScript
/// <reference path="../../typings/index.d.ts" />

module NewsModule{
    interface IFullUserNameScope extends ng.IScope{
        userId: number;
        firstName: string;
        lastName: string;
    }

    export class FullUserNameDirective implements ng.IDirective{
        public restrict = "E";
        public replace = true;
        public scope = {
            userId: "="
        };

        public template = "<span>{{ firstName }} {{ lastName }}</span>";

        constructor() {
        }

        public link = (scope: IFullUserNameScope, 
                       element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
            var userDetails = this.GetUserDetails(scope.userId);
            scope.firstName = userDetails[0];
            scope.lastName = userDetails[1];
        };

        private GetUserDetails(userId: number): string[]{
            switch(userId){
                case 0:
                    return ["Gidget", "Thomson"];
                case 1:
                    return ["Emilia", "Cornell"];
                case 2:
                    return ["Cyril", "Wallen"];
                case 3:
                    return ["Andre", "Gunderson"];
                case 4:
                    return ["Joi", "Kruse"];
                case 5:
                    return ["Alexander", "Leon"];
                case 6:
                    return ["Emile", "Decker"];
                case 7:
                    return ["Idell", "Rosenberg"];
                case 8:
                    return ["William", "Bower"];
                case 9:
                    return ["Deb", "Royal"];
                case 10:
                    return ["Trista", "Grubb"];
            }
        }

        public static Factory(): FullUserNameDirective{
            return new FullUserNameDirective();
        }
    }
}

OK, that needs a little explanation.

The FullUserNameDirective implements IDirective interface. If you take a look at this interface, you will realize that all of the properties are optional so in fact, it is not necessary to implement this interface. In this example, I added it so it would be easy to see what are the allowed directive properties.

Then there are some Angular-specific properties (implemented by the IDirective interface):

  • Restrict is the usage of the directive - "E" stands for the Element
  • Replace means that original directive would be replaced by the elements in template field
  • Scope defines values passed to the directive, in this case, it is userId property that is necessary
  • Template is the result Html that would be rendered in view.

In this case, we don't need to inject any specific services to the directive, so the constructor is empty.

What is really important (and I have struggled with it a long time) is the link method.

This method is executed when the directive is being rendered. It works like a constructor, but in the method arguments, there are values passed with the scope and other useful properties.

The scope element could be of type any, but it is not a good practice in the Typescript, so instead dedicated interface is defined above, IFullUserNameScope that extends IScope. Extending IScope is not necessary, but in some cases, it could be useful to get access to base scope properties or methods.

After all, there is a public static Factory method that returns FullUserNameDirective. The vital fact about the directives is that the same directive can be displayed many times in one view (e.g., in the ng-repeat). Each directive should have isolated scope, as it is independent element. So the angular is executing Factory method as initialization for each of the directives instead of constructor method as in controllers or other services.

The factory method also allows encapsulating initialization within the directive, so in the initialization there is no need to know what dependencies are used in the directive. The initialization is as simple as:

JavaScript
app.directive("fullUserName", NewsModule.FullUserNameDirective.Factory);

Note that in this case, name of the directive is written in pascal case ("fullUserName"). The angular is automatically changing pascal case to words separated by dashes (kebab-case ?) as it is commonly used naming convention in HTML. So the first letter of the directive name is lower-case so it would match the convention.

Finally, usage of the directive in the view:

HTML
<li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
    <full-user-name user-id="news.userId"></full-user-name>
    <div ng-if="$index === 0">
        <h3 ng-bind="news.title"></h3>
        <p ng-bind="news.body"></p>
    </div>
    <span ng-if="$index > 0" ng-bind="news.title"></span>
</li>

Note that the userId parameter is also transformed to match the kebab-case convention and is referenced as user-id.

Finally, let's create the UserModel and move the GetUserDetails method to UserService, so the directive would be a bit cleaner.

JavaScript
module NewsModule{
    export class UserModel{
        public id: number;
        public firstName: string;
        public lastName: string;

        constructor(id: number, firstName: string, lastName: string) {
            this.id = id;
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }
}
JavaScript
/// <reference path="../models/userModel.ts" />

module NewsModule{
    export class UserService{
        public GetUserDetails(userId: number): UserModel{
            switch(userId){
                case 0:
                    return new UserModel(userId, "Gidget", "Thomson");
                case 1:
                    return new UserModel(userId, "Emilia", "Cornell");
                case 2:
                    return new UserModel(userId, "Cyril", "Wallen");
                case 3:
                    return new UserModel(userId, "Andre", "Gunderson");
                case 4:
                    return new UserModel(userId, "Joi", "Kruse");
                case 5:
                    return new UserModel(userId, "Alexander", "Leon");
                case 6:
                    return new UserModel(userId, "Emile", "Decker");
                case 7:
                    return new UserModel(userId, "Idell", "Rosenberg");
                case 8:
                    return new UserModel(userId, "William", "Bower");
                case 9:
                    return new UserModel(userId, "Deb", "Royal");
                case 10:
                    return new UserModel(userId, "Trista", "Grubb");
            }
        }
    }
}
JavaScript
/// <reference path="../../typings/index.d.ts" />
/// <reference path="../services/userService.ts" />
/// <reference path="../models/userModel.ts" />

module NewsModule{
    interface IFullUserNameScope{
        userId: number;
        firstName: string;
        lastName: string;
    }

    export class FullUserNameDirective implements ng.IDirective{
        public restrict = "E";
        public replace = true;
        public scope = {
            userId: "="
        };

        public template = "<span>{{ firstName }} {{ lastName }}</span>";

        constructor(private userService: UserService) {
        }

        public link = (scope: IFullUserNameScope, 
                       element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
            var userDetails = this.userService.GetUserDetails(scope.userId);
            scope.firstName = userDetails.firstName;
            scope.lastName = userDetails.lastName;
        };

        public static GetFactory(): (userService: UserService) => FullUserNameDirective{
            var factory = (userService: UserService) => new FullUserNameDirective(userService);
            factory.$inject = ["UserService"];

            return factory;
        }
    }
}

The only thing that requires explanation in the example above is the change in the FullUserNameDirective and the GetFactory method that evolved from the Factory method.

As now the directive requires the UserService, angular has to inject it into the directive. It is necessary to specify all dependencies, so angular could resolve them and inject when creating new objects of FullUserNameDirectives. So instead of the Factory method, there is a lambda expression that defines creation of the FullUserNameDirective. Then to lambda expression, a $inject property is assigned with all necessary dependencies. Thus the lambda expression is in fact a factory method that is returned by the GetFactory method.

Thanks to that, all dependency information is encapsulated in the directive, so any changes in the directive implementation would not require any changes outside the directive.

Component registration in the app.ts file is still very simple:

JavaScript
app.service("UserService", NewsModule.UserService);

app.directive("fullUserName", NewsModule.FullUserNameDirective.GetFactory());

Routing and Views

Now as the client-side of the application looks good, let's improve the view.

Currently there is only one view file - index.html that is responsible for many things and is very complex.

It is time to introduce routing.

To install ui-router, in the command line, execute:

bower install angular-ui-router --save

To get type definitions, in the command line, execute:

typings install github:DefinitelyTyped/DefinitelyTyped/
angular-ui-router/angular-ui-router.d.ts#47310a628aa6d2d7e58ac0463a6a2e0918954d9e

Unfortunately, the application is based on a bit old versions of libraries (angular and ui-router), so to get no compilation errors, I recommend using this version.

If the gulp works fine after restart, let's introduce some views and routing.

In the project directory, add views directory and then news directory. In news directory, create newsList.html.

HTML
<section>
    <header>
        <h1 ng-bind="News.helloWorld"></h1>
    </header>
    <ul>
        <li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
            <full-user-name user-id="news.userId"></full-user-name>
            <div ng-if="$index === 0">
                <h3 ng-bind="news.title"></h3>
                <p ng-bind="news.body"></p>
            </div>
            <span ng-if="$index > 0" ng-bind="news.title"></span>
            <input type="button" text="Show details" ui-sref="news.details({id: news.Id})"/>
        </li>
    </ul>
</section>

Basically, a NewsController has been moved from index.html into newsList.html, but there is no declaration of ng-controller. This will be set in the routing file.

There is also a button "Show details" that contains ui-sref directive. This is responsible for redirecting to another state.

As mentioned above, ui-router uses states to navigate inside application between views. State is a kind of a view, but with defined route and parameters.

Let's create routes.ts file in the scripts directory:

JavaScript
/// <reference path="../typings/index.d.ts" />

module NewsModule{
    export class RouteConfig{
        public static $inject = ['$stateProvider', '$urlRouterProvider'];

        constructor($stateProvider: ng.ui.IStateProvider, $urlRouterProvider: ng.ui.IUrlRouterProvider) {
            $stateProvider.state('news', {
                url: "/news",
                templateUrl: "views/news/newsList.html",
                controller: "NewsController as News"
            });

            $urlRouterProvider.otherwise("/news");
        }
    }
}

This class is responsible for routing definitions. $stateProvider and $urlRouterProvider are ui-router components and allows registration states and routes in the application.

We have declared news state with its url, path to template html in templateUrl and controller.

The urlRouterProvider otherwise method redirects to default state, when the route is incorrect and state is not recognized.

In the app.ts, it is necessary to register the RouteConfig as angular config.

JavaScript
app.config(NewsModule.RouteConfig);

Finally, to get our routing working, <ui-view/> directive is necessary in the index.html file:

HTML
<body ng-app="NewsApp">
    <ui-view/>
</body>

Ui-router is replacing the <ui-view/> element with current state's template. It is also resolving specified controller for the template.

It is possible to nest <ui-view/> inside the template and it is possible to make some state "abstract" as it cannot be navigated to (a parent view for example).

Cross Origin Request Error

When application uses ui-router to navigate between views, it is possible to get the Cross Origin Request Error, because ui-router is getting the template with xhr requests. If you were viewing application as a file (e.g. file:///C:/Projects/NewsApp/index.html#/), the error will occur. To deal with the error, it is necessary to host the application on the server. You can use IIS, tomcat or a lightweight library from NodeJs, such as http-server. To install http-server, execute in command line:

npm install http-server -g

After install, run the server on the specific port (it is not necessary, but I recommend specifying ports):

http-server -p 666

Now the application is available at the url http://localhost:666/#/news.

Notice that when wrong address is typed (e.g., http://localhost:666 or http://localhost:666/#/test) it is immediately redirected to a default state - news.

Ok, so if the application is now working, let's add new state - news details.

Create newsDetails.html file in the views/news/ directory:

HTML
<section>
    <header>
        <h1 ng-bind="NewsDetails.details.title"></h1>
    </header>
    <p ng-bind="NewsDetails.details.body"></p>
    <span>This post was created by <full-user-name user-id="NewsDetails.details.userId">
    </full-user-name></span>
    <input type="button" value="Close" ui-sref="news"/>
</section>

Now add new controller, NewsDetailsController in newsDetailsController.ts file:

JavaScript
module NewsModule {
    export class NewsDetailsController {
        public static $inject = ["NewsService"];

        public details: NewsModel;

        constructor(private newsService: NewsService){
            this.LoadNewsDetails();
        }

        private LoadNewsDetails(){
            this.newsService.GetNewsById(1)
                .then(success => {
                    this.details = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

Controller registration in app.ts:

JavaScript
app.controller("NewsDetailsController", NewsModule.NewsDetailsController);

And state registration in routes.ts:

JavaScript
.state('news.details', {
    url: "/details/{id}",
    templateUrl: "views/news/newsDetails.html",
    controller: "NewsDetailsController as NewsDetails"
});

Notice that the state name is absolute, it indicates that details state is a child of news state. On the other hand, the url is relative, just adds a suffix of "/details/{id}" to the path, but in the end, the url would be combined of all parents, so it would be like "#/news/details/{id}".

Another thing is the {id} parameter. This is the id of the news details. Given it that way, it can be retrieved in the NewsDetailsController.

To make it fully work, a newList.html needs a line of code at the end of the section:

JavaScript
<ui-view></ui-view>

Now when clicking button Show details, news details are shown below the news list. The ui-view element in parent view is a container for child view.

But there is an issue with the FullUserNameDirective as it does not show anything in the NewsDetails.

The problem is that getting User details is done in the link method. The link method is executed when the directive is drawn. But the user id is retrieved with small delay as it comes from the WebApi through Ajax.

To make it fixed, a userId parameter has to be "observable" and fire event in directive when is changed.

This implementation of link method in FullUserNameDirective adds watcher to userId parameter:

JavaScript
public link = (scope: IFullUserNameScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
    scope.$watch(() => scope.userId, 
           (newValue: number, oldValue: number, scope: IFullUserNameScope) => {
        var user = this.userService.GetUserDetails(newValue);
        scope.firstName = user.firstName;
        scope.lastName = user.lastName;
    });
};

The $watch method executes when the userId parameter changes value. New value, old value and the scope are passed to the callback method so we can easily detect changes and update scope parameters.

A newValue can be undefined, when the directive is initialized. Let's update the UserService to be vulnerable to this by adding a default in the switch:

JavaScript
default:
    return new UserModel();

The UserModel needs small change in the constructor too:

JavaScript
constructor(id?: number, firstName = "", lastName = "") {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
}

Now the User full name is properly displayed in the NewsList and NewsDetails.

But there is another bug in this application. NewsDetails is always with id=1 from the WebApi. Let's get the id parameter from router and pass it to NewsService. Changes in NewsDetailsController:

JavaScript
export class NewsDetailsController {
    public static $inject = ["NewsService", "$stateParams"];

    public details: NewsModel;

    private id: number;

    constructor(private newsService: NewsService, $stateParams: ng.ui.IStateParamsService){
        this.id = $stateParams["id"];
        this.LoadNewsDetails();
    }

    private LoadNewsDetails(){
        this.newsService.GetNewsById(this.id)
            .then(success => {
                this.details = success.data;
            }, error => {
                console.log(error);
            });
    }
}

The stateParams Service contains state parameters as a key-value dictionary. We can get the id parameter by its name (defined in the routes.ts file) and then pass it to NewsService in the LoadNewsDetails.

The NewsList view also need small change in the newsList.html:

HTML
<input type="button" value="Show details" ui-sref="news.details({id: news.id})"/>

Now correct details of the selected news are shown in the News Details section.

But to be honest, I don't really like it. I would like News Details to be shown as a new page, not a section below list.

To fix that, a news state would be the parent (and now the main state), a news.list state would be the child of news and the news.details would be another child of news state. This needs small changes in the routes.ts file:

JavaScript
$stateProvider.state('news', {
    url: "/news",
    template: "<ui-view/>",
    abstract: true
})
.state('news.list', {
    url: "",
    templateUrl: "views/news/newsList.html",
    controller: "NewsController as News"
})
.state('news.details', {
    url: "/details/{id}",
    templateUrl: "views/news/newsDetails.html",
    controller: "NewsDetailsController as NewsDetails"
});

The url in the news.list is empty, so when entering the #/news url, the news.list state would be triggered as the news state is marked as abstract.

The template of the news state is just <ui-view/> to render the child.

The Close button in the news details view also needs small change, as the news state is abstract and cannot be redirected to:

HTML
<input type="button" value="Back" ui-sref="news.list"/>

Possible Improvements

That's it! Fully working application with routing, template views, directives and the WebApi service usage is done. What is the most value, whole application is written with strongly-typed TypeScript.

The application does not look good, though. It can be improved with some bootstrap styles, angular ui-bootstrap directives and so on. Then all CSS files can be concatenated and minimalized by the gulp tasks.

But it is not the point of this tutorial, I will leave you with this as a homework. :)

The created project is attached to the article, so feel free to download and test it.

Hope you enjoyed the article and the tutorial. :)

Points of Interest

Unfortunately, from the time I did the project, there were many new releases of Angular. So there is a need to specify the version of angular and all components so they could work together.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland
Hello from Poland Wink | ;)

I have been a .NET Developer since 2012. Since then I have had an opportunity to work with many projects written in technologies such as WinForms, WPF, ASP.NET MVC and ASP.NET WebAPI.
Ocasionally I did some projects in my spare time (for fun, on the studies project or as a remote part-time job).
But first of all I love coding and the possibility to increase my programming skills.

For few years I have been into client-side coding in JavaScript or TypeScript with different frameworks. I try to be up to date with latest trendings but actually the client side world is moving so fast I barerly can keep up with it Smile | :)

Cheers,
Miłosz

Comments and Discussions

 
PraiseGood Job! Pin
Stephen Wu 74-Jan-17 7:47
Stephen Wu 74-Jan-17 7:47 
PraiseGreat! Pin
Piotr Izak3-Jan-17 2:30
professionalPiotr Izak3-Jan-17 2:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.