Because of certain obscure reasons, AngularJS needs to do very particular things with “this”. Shortly after taking up TypeScript and trying to feed in Angular ‘objects’, I learned about how “Fat Arrow Notation” (FAN) allows for a clean interface with Angular. It’s covered in earlier posts, but the essence is
// Module that uses the angular controller, directive, factory and service defined above. module AngularApp { // define how this application assembles. class AngularMain { serviceModule:ng.IModule; appModule:ng.IModule; public doCreate(angular:ng.IAngularStatic, tcontroller:Function, tservice:Function, tfactory:Function, tdirective:Function) { this.serviceModule = angular.module('globalsApp', []) .factory('GlobalsFactory', [tfactory]) .service('GlobalsService', [tservice]); this.appModule = angular.module('simpleApp', ['globalsApp']) .controller('MainCtrl', ['GlobalsFactory', 'GlobalsService', '$timeout', tcontroller]) .directive('testWidget', ['GlobalsService', tdirective]); } } // instantiate Angular with the components defined in the 'InheritApp' module above. Note that the directive and the factory // have to be instantiated before use. new AngularMain().doCreate(angular, InheritApp.TestController2, InheritApp.TestService, new InheritApp.TestFactory().ctor, new InheritApp.TestDirective().ctor); }
Basically, the Angular parts that need to new() components (Controllers and Services) get the function pointer to the class, while components that depend on the object already being created (Directives and Factories) have a function pointer passed in that returns an object that in turn points to the innards of the class.
So I refactored all my webGL code to FAN and lo, all was good. I made good progress on building my shiny 3D charts.
Well, charts are pretty similar, so I wanted to take advantage of TypeScript’s inheritance, make a BaseChart class, which I would then extend to Area, Bar, Column, Scatter, etc. What I expected to be able to do was take a base method:
public fatArrowFunction = (arg:string):void => { alert("It's Parent fatArrowFunction("+arg+")"); };
And extend it:
public fatArrowFunction = (arg:string):void => { super.fatArrowFunction(arg) alert("It's Child fatArrowFunction("+arg+")"); };
“Um, no.”, said the compiler. “super() cannot be used with FAN”.
“WTF?” Said I.
It turns out that this is a known not-really-a-bug, that people who are combining Angular and TypeScript run into. After casting around for a bit, I found the fix as well:
class Base { constructor() { for (var p in this) { if (!Object.prototype.hasOwnProperty.call(this, p) && typeof this[p] == 'function') { var method = this[p]; this[p] = () => { method.apply(this, arguments); }; // (make a prototype method bound to the instance) } } } }
Basically what this does is scan through the prototype list and set a bunch of fat arrow function pointers that point back to the prototype function. It seems that there are some people that complain, but as a developer who cut his teeth on C programming, I find function pointers kind of comforting. They become a kind of abbreviation of some big complex thing.
The problem is that the example doesn’t’ quite work, at least in the browsers I’m currently using (Chrome 41, IE 11, FF 36). Instead of pointing at their respective prototypes, all the pointers appear to reference the last item of the loop. And the behavior doesn’t show up well in debuggers. I had to print the contents of the function to see that the pointer named one thing was pointing at another. And this happened in a number of contexts. For example, this[fnName] = () => {this[‘__proto__’][fnName].apply(this, arguments);} gives the same problem.
After a few days of flailing and learning a lot, I went back to basics and tried setting the function pointers explicitly in the constructor of each class. It worked, and it wasn’t horrible. Then, and pretty much just for kicks, I added the base class back in with this method:
public setFunctionPointer(self:any, fnName:string):void{ this[fnName] = function () { //console.log("calling ["+fnName+"]") return self['__proto__'][fnName].apply(self, arguments); }; }
And gave it a shot. And it worked! I was pleasantly surprised. And because I’m an eternal optimist, I added the loop back, but this time using the function call:
constructor() { var proto:Object = this["__proto__"]; var methodName:string; for (var p in proto){ methodName = p; if(methodName !== 'constructor'){ this.setFunctionPointer(this, methodName); } //console.log("\t"+methodName+" ("+typeof proto[p]+")"); } }
And that, my droogs, worked.
I think it’s a prototype chaining issue, but I’m not sure how. In the non-working code, we’re basically setting this[fnName] = function () { this[fnName].apply(self, arguments)}. That should chain up to the prototype and work, but I don’t think it is. Rather, all the functions wind up chaining to the same place.
function Base() { var _this = this; for (var p in this) { if (!Object.prototype.hasOwnProperty.call(this, p) && typeof this[p] == 'function') { var method = this[p]; this[p] = function () { method.apply(_this, arguments); }; } } }
On the other hand, look at the code generated when we use the function we get the following:
var ATSBase = (function () { function ATSBase() { var proto = this["__proto__"]; var methodName; for (var p in proto) { methodName = p; if (methodName !== 'constructor') { this.setFunctionPointer(this, methodName); } } } ATSBase.prototype.setFunctionPointer = function (self, fnName) { this[fnName] = function () { //console.log("calling ["+fnName+"]") return self['__proto__'][fnName].apply(self, arguments); }; }; return ATSBase; })();
Now, rather than starting at the root, the actual call is done in the prototype. I think this may cause the chain to start in the prototype object, but then again, looking at the code, I don’t see why that should be the case. One clear difference is the fact that in the first version, “this” can be in two closure states (this[p] = function (){method.apply(_this, arguments);};). So it could be closure is behaving in less than obvious ways.
Unfortunately, we are at the point in development where something works, so it’s time to move on. Maybe later after the codebase is more mature, I’ll come back and examine this further. You can explore a running version here.
Reblogged this on Dinesh Ram Kali..