Converting an Angular app to TypeScript – part 2

Introduction

In the previous article, I explained how to configure the build process and convert an Angular 1 controller to TypeScript. This article will continue building from where we left, and explain how to convert a service and a directive.

Converting a service

As the logger service is used by the admin controller that was converted previously, let’s convert this service to TypeScript. As previously, the first step is to rename the file to logger.ts. Next, let’s create a module called app.blocks, to match the folder structure. Then, let’s create the interface defining the members to be implemented:

///<reference path="../../../../../typings/angularjs/angular.d.ts"/>
module app.blocks {
    'use strict';
    export interface Ilogger {
        showToasts: boolean;

        error: (message: string, data?: any, title?: string) => void;
        info: (message: string, data?: any, title?: string) => void;
        success: (message: string, data?: any, title?: string) => void;
        warning: (message: string, data?: any, title?: string) => void;

        log: ng.ILogCall;
    }
}

As you can see, some parameters on the methods are appended with a question mark. This lets TypeScript know that these parameters are optional and can be omitted.

Now, on with the concrete implementation of the interface:

class logger implements Ilogger {

    static serviceId = 'logger';
    showToasts: boolean;
    log: ng.ILogCall;

    static $inject = ['$log', 'toastr'];
    /* @ngInject */
    constructor(private $log: ng.ILogService, private toastr: any) {
        this.showToasts = true;
        this.log = $log.log;
    }

    error(message: string, data?: any, title?: string): void {
        this.toastr.error(message, title);
        this.$log.error('Error: ' + message, data);
    }

    info(message: string, data?: any, title?: string): void {
        this.toastr.info(message, title);
        this.$log.info('Info: ' + message, data);
    }

    success(message: string, data?: any, title?: string): void {
        this.toastr.success(message, title);
        this.$log.info('Success: ' + message, data);
    }

    warning(message: string, data?: any, title?: string): void {
        this.toastr.warning(message, title);
        this.$log.warn('Warning: ' + message, data);
    }

    static instance($log: ng.ILogService, toastr: Toastr): Ilogger {
        return new logger($log, toastr);
    }
}

This class implementation is very similar to the controller converted previously. The type of toastr is any, we will come to this in a moment.
It should be noted that a static method called instance was added. This will be used to register the factory in Angular. This registration is achieved by:

angular
        .module('blocks.logger')
        .factory(logger.serviceId, logger.instance);

The use of the instance method is a requirement from Angular, that needs a method that returns something as the registration function for factories and directives. This registration pattern is slightly different than the one used for controllers.

Let’s then add typings for toastr. In a command prompt, go to the root directory of your project and type

tsd install --save toastr

Then, in your logger file add a reference to toastr types at the top of the file:

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

Finally, change the type of toastr in the constructor to Toastr.

We can now go back to admin.controller.ts and add a reference to the logger at the top of the file:

///<reference path="../blocks/logger/logger.ts"/>

We can then change the type of logger to be app.blocks.Ilogger. Complete namespacing is required as the types are defined in different modules.

Converting a directive

Finally, let’s convert the htTopNav directive, as it is pretty simple. To make it more compliant to the modern style, I will create an actual controller for the directive. As previously, the first steps are to rename the file, create a module to encapsulate the code and add the reference to angular type definition:

///<reference path="../../../../typings/angularjs/angular.d.ts"/>
module app.directives {
    'use strict';

}

Let’s then create an interface for the directive, that extends Angular native directives:

export interface IhtTopNavDirective extends ng.IDirective {

}

This interface does not have any property other than the native ones, and is therefore empty. Now for the core of the directive, let’s create a class containing the required properties:

class htTopNav implements IhtTopNavDirective {
    static directiveId = 'htTopNav';
    bindToController = true;
    controller = TopNavController.controllerId;
    controllerAs = 'vm';
    restrict = 'EA';
    scope = {
        'navline': '='
    };
    templateUrl = 'app/layout/ht-top-nav.html';

    static $inject =[];
    constructor() {
    }

    static instance(): IhtTopNavDirective{
        return new htTopNav();
    }
}

angular
    .module('app.layout')
    .directive(htTopNav.directiveId, htTopNav.instance);

Notice that the TopNavController function disappeared, as it will be created as a separate class (as can be seen from the controller property, referring to a static property of the controller) and registered as an actual controller.

A new static property (directiveId) was created to reference the name of the directive inside the directive itself and not as a string while registering the directive.

For the same reasons as for the services, an instance method is used to register the directive within Angular.

Let’s then create the controller for that directive:

interface ItopNavController {

}

class TopNavController implements ItopNavController {
    static controllerId = 'TopNavController';
}

angular.module('app.layout')
    .controller(TopNavController.controllerId, TopNavController);

The controller is actually pretty simple in this case, but could be more complex in the case of a more advanced directive. Using a controller in combination with a directive is considered best practice and a way forward to migrating to Angular 2. Having a controller basically lets you encapsulate your logic outside of the link function, that should only be used to manipulate the DOM.

Conclusions

This article explained how to convert services and directives from native JavaScript to TypeScript. Though the cases presented were pretty simple, the same principles can be applied to more complex cases.

The use of controllers in conjunction with directives makes for a path to Angular 2 and the components idea, and leads to generally very lean directives (most of my cases my directives only contain plumbing to bind to the controller, and no link function at all).

This article concludes the conversion of an Angular 1 application to TypeScript. I hope you learnt some things. Do not hesitate to contact me if things are unclear or incorrect, or you think some things should be changed.

Advertisements
Leave a comment

1 Comment

  1. Converting an Angular app to TypeScript – part 1 | Wandering in the community

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

%d bloggers like this: