Convert AtAGlance controller to typescript

Issue #67 resolved
Brian Lewis repo owner created an issue

Here's the steps:

1) create a new file in the relevant typescript folder under src/app/ts/<corresponding folder>/<same name>.ts

The js definition consists of a function declaration and the angular assignments in a closure (IEFE)

1) in typescript, the closure becomes a module block: module Pineapples { }

2) in typescript, add a class representing the controller. Doesn't generally need to be exported:

class AtAGlanceController {

}

3) After the class to register the controller with the injector:

angular
    .module("pineapples")
    .controller("AtAGlance", AtAGlanceController);

Notice you don;t use the [] syntax to supply the injections to the controller like the js does.

5) At this point, save the new file, add it to tsconfig.json to include it in the project, and build. This registers that this file belongs to this project, and makes intellisense work from here in. In particular, the symbol angular should now appear defined.

6) In the class, create the static $inject member. This is the injection tokens required by the class. In the js, these are added when the controller is created.

static $inject = ["$scope", "$state", ..... whatever];

4) create the constructor in the class, which now takes arguments for each injected item

constructor(scope, state, .......) {
}

The constructor will largely copy the js function.

5) in js you can assign properties to the controller at will...

this.prop = value;

But in ts, you'll need to explicitly define any public, protected or private properties on the class before you can assign

class AtAGlanceController {
    public myProp: string;

...
   constructor (....) {
       this.myProp = theValue;
   }
}

HOWEVER, as a useful shortcut, you can expose any arguments to the constructor as properties by scoping them like this

constructor ( public lookups) {
...
   }

lookups is a public property of the class now. In particular this makes it available to use in the view:

{{ vm.lookups.cache['surveyYears'] }}

You should now be able to proceed on and get a clean compile and working version.

If you don't want to delete the js starting point just yet, just remove or comment out the registration of the controller in that closure, to make sure you register your typescript version

Take a look at the generated js and compare to the original handcoded js.

Finally, to get more advantage from typescript, take the time to type arguments that will not be implicitly typed. This will give you instellisense and compile time checking.

In particular, try to type all injections to the constructor that are angular built in services:

constructor(scope: ng.IScope, state: ng.IStateService....) {
}

the other main issue is this I don't find the need to bind or do tricks like

var self = this;

You can use this safely referring to the controller everyehere except in callback functions (notably promise.then() or array.forEach(...) etc)

For callbacks, express them as lambda functions; ie

public foo(bar) {

}

should become

public foo = (bar) => {

}

If you look at the generated javascript, you'll see in this case it outputs var _this = this;

For me, losing this this is far and away the most common error I find moving js code to typescript . The property of lambda functions to implicitly bind this is a really important thing to remember.

Comments (8)

  1. Ghislain Hachey

    I've started this. I did a first try to converting the AtAGlanceController to TS and it seems to work. Could you have a look. I'll continue with some types declaration and such but I have already pushed a issue67 branch you can have a look and tell me if you think I am in the right direction. And thanks for the steps above, really useful.

  2. Brian Lewis reporter

    Great, this is looking good. One other thing you can do:

    typescript has a getter / setter syntax (see for example here) that compiles down to object.defineProperty . So try defining the public properties selectedYear and baseYear as public properties in this way. As you suggested in your code _baseYear and _selectedYear become private variables wrapped by these public properties.

    Using getters/setters like this is a way to respond to a property change without introducing an angular $watch.

  3. Ghislain Hachey

    I pushed a couple of commits adding more type declations and notes for future JS->TS and also fixing an issue with action compiling to var action on the controler and not this.action (the drilldown to view population was not working). I'll try your getter/setter syntax conversion now.

  4. Ghislain Hachey

    ok, just about to push this. It looks like it is working but I have to admit I do not quite understand why vm.selectedYear and vm.baseYear are still available in the view as numbers (years) as they are now only defined as getters/setters manipulating private variable _selYear and _bYear. In other words, the translation I did just feels wrong. Can you have a good look and provide small explanation?

  5. Brian Lewis reporter

    get and set are language structures defined by typescript to allow you to add a property to a class. You can use this property exactly as if it were defined as a simple variable property:

    in particular

    public set selectedYear(newValue) {
    }
    

    is invoked when you say

    controller.selectedYear = newValue;

    and this is what we get when binding from the view using ng-model="vm.selectedYear"

    In fact , the compiler generates this javascript from the getter/setter pair:

    Capture.PNG

    which is almost exactly what we had before.

    The point of using getter /setter is that the setters do a little more than just set the private variables - both the setters contain a little bit of processing to go and get the relevant data cache for the year and to ensure that the selectedYear is greater than the baseYear.

        set selectedYear(newValue: number) {
          this._selYear = newValue;
          this.selectedYearData = this.ataGlanceMgr.get(this.selectedYear, this.selectedEdLevel);
          if (this.selectedYear <= this.baseYear) {
            this.baseYear = this.selectedYear + 1;
          }
        }
    

    One point of "etiquette" - I would scope both the getter and setter pairs as public. It doesn't make any difference to the generated javascript, but I think it underlines the fact that we are expecting the view to have access to these properties.

    It would make a difference to the typescript compiler if we were to export this class and refer to those properties in other typescript code in another file. We'd get a compile error if the properties referred to were not public. But I find that many angular objects - controllers, directives, components - only need to be visible in the module block (ie file) in which they are defined and registered. And if they don't need to be exposed, then better that they aren't.

    One useful counterexample is when you want to derive a controller from a parent controller (or service from a parent service.) In that case, you will want to export the base class, so that it is available to you to extend in another file.

    Services are consumed by other typescript code - specifically, by the controllers into which they are injected. So it can make sense to expose a class definition or an interface representing a service, so that the injected service can be typed with that interface:

    Strictly speaking it's better to use an interface for this purpose - because it allows mocking.

    So from this it would be good to go straight into #66 , where you will inject Lookups into the controller, and you can strongly type it as it is here:

       static $inject = ['$scope', 'theFilter', 'Lookups', 'findConfig'];
    
        constructor($scope, public theFilter: Sw.Filter.IFilter, protected lookups: Sw.Lookups.LookupService
          , public findConfig: Sw.Filter.FindConfig) {
    
  6. Ghislain Hachey

    Yeah, I had a pretty good picture of the purpose and benefits of the accessors. I also thought of making them public but the TS docs did not have it so I left it. I've added it and just pushed.

    My only point of confusion is that before we had a this.selectedYear property and a Object.defineProperty(this, 'yearSelected'). My Javascript understandind is not that deep. Were those effectively the same? In other words, was Object.defineProperty(this, 'yearSelected') simply overriding silently this.selectedYear? This is why I could effectively get rid of it (in fact I was forced to by compiler check). Also the following is still confusing to me.

    in particular

    public set selectedYear(newValue) {
    }
    

    is invoked when you say

    controller.selectedYear = newValue;

    Though will probably be clear tomorrow morning.

  7. Log in to comment