TypeScript classes as angular controllers

Topics: General
Jul 7, 2013 at 11:03 PM
I was able to get a TypeScript class converted to an angular controller by calling the constructor on the passed in $scope. This lets you avoid having to create a separate interface to list all the fields you're going to assign to scope, and if you have a base class definition for angular's scope, you can get intellisense and type checking for fields/methods assigned by angular. But, I'm not sure if this method will work in the general case - with various class heirarchies, generics, etc. Does anyone see any reason why this wouldn't work for some particular scenario? warning, typing this on a phone and potentially mucking up the syntax:
class SimpleController {
public field: string;
constructor($http: any) { 
this.field = "hi";
}
}

angular.module("app").controller("SimpleController", ["$scope", "$http", function($scope, $http) {
var args = Array.prototype.splice.call(arguments).splice(1);
SimpleController.call($scope, args);
}]);
Coordinator
Jul 8, 2013 at 6:58 PM
Not quite sure I follow. Are you saying you want a way of specifying only some of the interface without having to specify all of the member?

If so, one way you can do this is to access the members you want to use untyped using the x["member"] syntax, which will assume 'any'.
Jul 9, 2013 at 1:47 AM
Edited Jul 9, 2013 at 6:20 AM
Thanks, I wasn't aware that x["member"] syntax assumes any, which is very helpful. In this case, however, my goal is to define angular controllers (which .Net developers would call view-models) as TypeScript classes. I think I need to provide a little bit of context. AngularJS supports data-binding that is somewhat MVVM-ish. Angular templates data-bind to fields and functions of the $scope. There may be child scopes of parent scopes, which prototypically inherit from a parent scope, or isolated scopes which do not inherit from a parent scope.

The $scope is the "view-model", so to speak. $scopes are instantiated by Angular and injected into Angular Controllers, which are effectively just functions with the current $scope passed in as a method parameter. In my example above, the function with $scope and $http arguments is the controller. $scope is the view model. The controller's primary job is just to add fields and functionality to the view-model - which is why I use "controller" and "view-model" interchangeably.

The array definition (["$scope", "$http", function($scope, $http) {...}] is part of Angular's dependency injection. It just tells angular to inject the current $scope and the $http service into the function. This makes it so you can minimize your code and still use dependency injection (because the $http and $scope argument names would presumably be shortened).

My goal is to define a TypeScript class to use as the view-model (on $scope) without casting $scope to any first. The problem is that Angular instantiates $scopes for you, and passes them in via dependency injection. They already may have their own prototypes representing the $scope hierarchy, up to the shared methods on all $scopes (such as the $watch function). It does this because it manages the $scope hierarchy based on directives/templates/etc.. in the markup.

One way I could do this is to just create a new instance of a TypeScript class and assign that instance to the $scope variable:
// controller.js
angular.module("app").controller("SimpleController", ["$scope", function($scope) {
$scope.vm = new SimpleController();
}]);

// template.html
<span>Value of SimpleController.field: {{vm.field}}</span>
But this is somewhat cumbersome because, in the markup, I will need to prefix all the bindings with the name of the field (in this case "vm"). It's an acceptable workaround, but I wanted something slightly more integrated. Instead of assigning an instance of a class to the $scope variable, I wanted the class to BE the $scope. The only way I could think of to make this work is to call the constructor function explicitly, passing the $scope as "this" and the rest of the dependency injection arguments as arguments to the class constructor (in my example, just the $http service).

But I had doubts that this would work, so that's why I'm posting here. I'm especially concerned that TypeScript classes which already extend other classes might just break horribly if I tried to call the constructor explicitly on an object which already prototypically inherits from something else.

Edit: I should probably give an example of what I'm trying to avoid by going this route. If you look at the DefinatelyTyped example for defining AngularJS controllers in typescript (https://github.com/borisyankov/DefinitelyTyped/tree/master/angularjs), you see that they recommend defining an interface for everything you're going to assign on $scope, then assigning those things in the controller. The controller function looks like normal, except that you set the type of $scope to the interface you define (which extends ng.IScope).
interface ICustomScope extends ng.IScope {
    title: string;
}

angular.module("app").controller("Controller", ["$scope", function Controller($scope: ng.ICustomScope) {
    $scope.$broadcast('myEvent');
    $scope.title = 'Yabadabadu';
}]);
This can be a real pain to work with, because every time you want to add a new function or field to the scope, you have to modify the interface (ICustomScope in this example). What I want to do instead is something like this:
function MergeClassIntoScope(theClass: any, scope: ng.IScope, args: any[]) {
  // What do I do here?
}

class Controller extends ng.IScope {
    title: string = "Yabadabadu";
    constructor($http: ng.IHttp) {
        this.$broadcast('myEvent');
    }
}

angular.module("app").controller("Controller", ["$scope", "$http", function($scope: ng.IScope, $http: ng.IHttp) {
    MergeClassIntoScope(Controller, $scope, [$http]);
}]);
Jul 11, 2013 at 10:06 AM
You may find this video useful. He defines controllers in typescript similar to your last implementation. Assigning $scope.vm = this;

http://www.youtube.com/watch?v=WdtVn_8K17E