AngularJS 1.x and TypeScript: The biggest tutorial, pt. 1

Hiya!

Today I'm going to write some words about creating AngularJS applications with TypeScript. Recently a lot of people told me they don't like writing in AngularJS only because its syntax is hard to remember, especially for directives. Well I have to agree but I write applications using this Google's framework for about two or three years so I have almost no problem to write anything with that.

AtScript

As you may know, few days ago Google announced beta version of Angular 2. In their first presentations developers was showing a sugar-syntax code which was transcompiled to JavaScript to be runnable by a browser. Classes, annotations, import keywords - goodness already known from Java with elements known from any other OOP language. The language has been presented and called as AtScript. Developers were frightened that they would have to learn yet another language to continue working with the framework they have loved so much, moreover AtScript hasn't been documented anywhere. There could be found only some propositions to new language's syntax. Just after the news about EcmaScript 6 have started to be announced more and more Google decided to put down the cover of mystery and announce that AtScript is actually the... TypeScript. Developers has fallen in love with new syntax very quickly and wanted to make their existing Angular applications that clear right now. Well it's actually possible with TypeScript! Recently I've finished an application written completely in TypeScript - server side as well as front one. For browser scripts I used AngularJS and I must say it looks awesome, much cleaner and easier to develop. Imagine already mentioned directives, probably the most complex thing in Angular, written with TypeScript goodness. It's possible right now!

Let's start!

Let's start with application initialization. I'll use bower only because of my own habits but I know it's better to use npm even for browser-oriented packages. It's your choice but remember to keep consistency and adaption of paths to node_modules directory instead of bower_components (or whatever else defined in your .bowerrc file.

So obviously we know we want to use the typescript package:

npm install typescript --save  

... and some frontend packages. First of all, AngularJS but let's also add Twitter Bootstrap for easy prototyping:

bower install angular --save  
bower install bootstrap --save  

Definitions of external packages would be indispensable here. We can write some definitions file for Angular but... why? Someone's already done that for us :) The TypeScript Definition Manager will be helpful here. Once you have it installed, just run:

tsd install angular jquery --save  

I've included also jquery package since it's required by angular.

So we have TypeScript compiler, AngularJS package, definitions and we can finally begin to play with the code. Let's start with something really simple.

We're going to write a shopping application with products, categories and a shopping cart. Let's create layout file with angular and simple markup:

<!-- app/index.html -->  
<html>  
    <head>
        <title>My Awesome Shop</title>
        <link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.min.css"/>
    </head>
    <body>
        <div class="container">
            <div class="jumbotron" id="header">
                <h1>My awesome shop</h1>
                <p>This is my shop, welcome!</p>
                <a href="#" class="btn btn-success pull-right">My cart</a>
            </div>

            <div class="col-lg-12" id="content">
                <div class="row">
                    <h1>Products available: </h1>
                    <!-- Products list will go here -->
                </div>
            </div>
        </div>
        <script type="text/javascript" src="../bower_components/angular/angular.min.js"></script>
    </body>
</html>  

Nothing special so far. I'd like you to notice few sections here to not to paste the whole code everytime I'll change it:

  • #header - the header of our app with title, subtitle and My Cart button
  • #content - the content of our app, will contain products listing
  • Line 20: The scripts, currently only angular lib.

Ok, it's time to add some products! We are going to create a service class to keep responsibility of fetching and keeping products in one place. Create a file in app/services called products.ts:

// app/services/products.ts

/// <reference path="../../typings/tsd.d.ts" />

module MyShop {  
    export class Product {
        name: string = '';
        price: number = 0;
        description: string = '';
    }

    export class Products {
        private products: Product[];

        getProducts(): Product[] {
            return null;
        }
    }
}

What actually happened? In line 3 we've imported tsd definitions. When you install definitions using tsd, it creates a file consisting of references to all installed files. For now it should look like this:

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

Next, we have created a module. Modules in TypeScript is a way of grouping code. It's like namespaces in PHP or C# or packages in Java.

In line we create a Product model class. Note the export keyword. It's important to point that if we export a class (or a variable, function, interface, whatever you want) it's available in other files. Product model consists only of three basic properties like name, its price and description. For now it's enough.

The most important thing is in line 12. Here we have our ProductsService. It keeps our products in private products variable (of array of Products type) which can be accessible through getProdcuts method. This class will become an angular service soon.

Tests first!

You may wonder why I've left the getProducts method empty (returning null). That's because I'd like you to write some unit tests for that. It's great practice to use TDD approach, especially in AngularJS apps. The best and the most reliable tool for unit tests within AngularJS application is Karma. By default it uses Jasmine as testing framework.
I'll write another article soon, how to install and configure Karma to use it with AngularJS and TypeScript, so stay tuned. Now let's write some basic unit test for our existing yet not implemented method.

Create a file products_test.ts in test/services directory:

// test/services/products_test.ts

/// <reference path="../../typings/tsd.d.ts" />
/// <reference path="../../app/services/products.ts" />

describe('ProductsService', () => {  
    it('should return an empty array if there are no products', () => {
        var productsService = new MyShop.ProductsService();
        var products = productsService.getProducts();

        expect(products.length).toBe(0);
    });
});

Remember when I wrote about modules? Look at line 8 where ProductService is instantiated. I wrote module name before class name because our tests are not in MyShop module.
I think everything is clear here. We expect that if there are no products, the getProducts method will return an empty array. Let's compile and run our test:

# compilation with some task runner here...
node_modules/.bin/karma start  
# ... or also by task runner, e.g. grunt:
# grunt karma:unit

Our tests fail because our method still return null so obviously it doesn't contain length property. Let's fix that!

// app/services/products.ts
// ...
    export class ProductsService {
        private products: Product[] = [];

        getProducts(): Product[] {
            return this.products;
        }
    }
// ...

One more try for karma... and our test goes green! Hurray!
So far so good, but our service is completely useless. To set up some products, let's add a temporary method to fill in the products array. But first, the test:

// test/services/products_test.ts

describe('ProductsService', () => {  
    // ...
    it("should return products it has", () => {
        var phoneProduct = new MyShop.Product();
        phoneProduct.name = "jPhone 8";
        phoneProduct.price = 999.99;
        phoneProduct.description = "A super modern smartphone!";

        var tvProduct = new MyShop.Product();
        tvProduct.name = "GL 610";
        tvProduct.price = 1119.99;
        tvProduct.description = "UltraHD TV with super awesome remote controller made of glass!";

        var laptopProduct = new MyShop.Product();
        laptopProduct.name = "Venolo G510";
        laptopProduct.price = 345.99;
        laptopProduct.description = "New Venolo's product, super thin and super fast laptop!";

        var products: MyShop.Product[] = [
            phoneProduct,
            tvProduct,
            laptopProduct
        ];

        var productsService = new MyShop.ProductsService();
        productsService.setProducts(products);

        var actualProducts = productsService.getProducts();
        expect(actualProducts).toEqual(products);
    });
});

Ok, so we assume that ProductService has a method setProducts that takes a Product[] array and puts it in products property. The compilation will fail because the method doesn't exist yet. Let's fix it by creating it, so far with no body to be sure our test will fail first:

// app/services/products.ts
setProducts(products: Product[]):void {}  

Now compilation will pass but karma won't. The log tells us the truth: getProducts returns an empty array while we expect the products we passed to setProducts method. First, let's implement the setProducts method:

// app/services/products.ts
setProducts(products: Product[]): void {  
    this.products = products;
}

Our tests should pass now so we are sure that our ProductsService works perfectly.

Did you forget about Anagular...?

Easy, of course not. Let's create our first angular module, of course in TypeScript, to join the service into our application. Create a file in app directory called my_shop.ts with the following content:

// app/my_shop.ts

/// <reference path="../typings/tsd.d.ts" />

module MyShop {  
    var application = angular.module('MyShop', []);
    application.service('ProductsService', ProductsService);
}

Some time ago I had a bad habit. When I was creating, for example, an angular service, in the declaration file I was putting the angular.module and then called service. As you can see, we've created a ProductsService POJO class, completely independent from framework and we've put the connection part into separate file. It gives us possibility to use our classes in other project or in other angular modules with no change in source code. In the snippet above we've assigned a reference to ProductService class, which is compiled to prototyped function (in case of compilation to ES5) so it's exactly what we want to achieve.

Red, green, REFACTOR!

Since our tests pass and ProductsService became a part of a MyShop angular module, we can adapt our tests to keep consistency in the future and take the benefits of AngularJS' dependency injection mechanism. First, let's install angular-mock with tsd declarations to make it possible:

bower install --save angular-mocks  
tsd install angular-mocks --save  

Then adapt your tests:

// test/services/products_test.ts

/// <reference path="../../typings/tsd.d.ts" />
/// <reference path="../../app/my_shop.ts" />
/// <reference path="../../app/services/products.ts" />

describe('ProductsService', () => {  
    var ProductsService: MyShop.ProductsService;

    beforeEach(angular.mock.module('MyShop'));
    beforeEach(inject([
        'ProductsService',
        (p) => {
            ProductsService = p;
        }
    ]));

    it('should return an empty array if there are no products', () => {
        var products = ProductsService.getProducts();

        expect(products.length).toBe(0);
    });

    it("should return products it has", () => {
        var phoneProduct = new MyShop.Product();
        phoneProduct.name = "jPhone 8";
        phoneProduct.price = 999.99;
        phoneProduct.description = "A super modern smartphone!";

        var tvProduct = new MyShop.Product();
        tvProduct.name = "GL 610";
        tvProduct.price = 1119.99;
        tvProduct.description = "UltraHD TV with super awesome remote controller made of glass!";

        var laptopProduct = new MyShop.Product();
        laptopProduct.name = "Venolo G510";
        laptopProduct.price = 345.99;
        laptopProduct.description = "New Venolo's product, super thin and super fast laptop!";

        var products: MyShop.Product[] = [
            phoneProduct,
            tvProduct,
            laptopProduct
        ];

        ProductsService.setProducts(products);

        var actualProducts = ProductsService.getProducts();
        expect(actualProducts).toEqual(products);
    });
});

Also, remember to adapt your Karma config to have angular-mocks included. You may also encounter problems with non-existent object during the tests - be sure that my_app.js compiled file is being loaded as the last element.

The amount of code

Maybe you've noticed that test case checking for existing products is a bit too long. The most of its code consists of creating products. Let's make it less complex by adding an ability to instantiate Product model parameters in the constructor:

    export class Product {
        constructor (private name: string, private price: number, private description: string) {}
    }

Then we can change our too long test to the following:

    it("should return products it has", () => {
        var products: MyShop.Product[] = [];

        products.push(new MyShop.Product("jPhone 8", 999.99, "A super modern smartphone!"));
        products.push(new MyShop.Product("GL 610", 1119.99, "UltraHD TV with super awesome remote controller made of glass!"));
        products.push(new MyShop.Product("Venolo G510", 345.99, "New Venolo's product, super thin and super fast laptop!"));

        ProductsService.setProducts(products);

        var actualProducts = ProductsService.getProducts();
        expect(actualProducts).toEqual(products);
    });

Number of *.d.ts references

You may noticed the number of references comments in the beginning of each file. It's a good practice to keep them all in one place - auto generated by tsd as well as application's ones.
Let's have a look at test/services/products_test.ts file:

// test/services/products_test.ts

/// <reference path="../../typings/tsd.d.ts" />
/// <reference path="../../app/my_shop.ts" />
/// <reference path="../../app/services/products.ts" />

There's 3(!) references to *.d.ts and *.ts files. We can shorten it into one. Create a file typings.ts somewhere in your project. I'll do it in app directory.

/// <reference path="../typings/tsd.d.ts" />
/// <reference path="my_shop.ts" />
/// <reference path="services/products.ts" />

Now we need to change all references in our files to use our new typings. Replace list of those comments to something like that:

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

You need to update my_shop.ts, products.ts and products_test.ts.

Now it looks much cleaner don't you think?


Damn, you wrote an another essay!

Please accept my apologies. I wanted it to be short but I failed again. At first I wanted to write whole tutorial in a single article, then I've decided to write only about services but after I've seen how much I wrote I decided to split it even more. So I've added the "the biggest tutorial" phrase to the title... I have such problem that if I want to show someone some new things I am experienced with, I talk/write a lot, as you've probably noticed. So please accept my apologies. This is just an introduction so it has to be long if you want to understand everything. We didn't even updated our template to show practices. Please be patient, we'll do this and after all parts you'll become a better developer.

Source code

You can find a source code for this tutorial at https://github.com/eps90/Angular-1.x-and-TypeScript-tutorial/tree/part-1
This repo will be updated and tagged to allow you follow this tutorial part-by-part. Tag for this article is called part-1. You can clone the repo by calling git clone and checkout to certain tag by git checkout:

git clone https://github.com/eps90/Angular-1.x-and-TypeScript-tutorial.git  
cd Angular-1.x-and-TypeScript-tutorial  
git checkout part-1  

Next parts

I have a plan to describe the whole application that you can write. It had to be an Angular to TypeScript tutorial but so far it became a Angular/Typescript/Karma/TDD tutorial. I think I'll continue with that to not to learn you bad practices, like in the other blogs.

I'll put some agenda to what I want to write in the next post, I'll update the links later, so you can pass the whole tutorial part-by-part. So I'm going to write about:

  • Introduction (this article)
  • Dependency injection - we'll talk about using other services within our code in the most clean way you've ever seen.
  • Directives - developer nightmare will end soon.
  • Filters - pretty simple and handy but still can be improved
  • Other angular goodness - values, constants, config, etc.

Moreover I plan to write an article introducing you to testing in Karma with TypeScipt. I think I'll write it soon to not to confuse you while reading this article.

Stay tuned and Merry Christmas!!!


Update 2015-12-20

  1. I've added yet another section to Red, green, REFACTOR! chapter about Number of *.d.ts references
  2. You can view the whole source code in the link provided in the section above Source code