2

I'm trying out AngularJS and I'm attempting to use TypeScript classes to get prepared for a large project. The problem I'm having is that the this of the controller class is not being binded to the scope inside the portion of the DOM where the ng-controller directive is.

Starting with the following TypeScript code:

angular.module('testApp')
.controller('TypeScriptController', () => new TypeScriptController())

// Controller implementation via TypeScript
class TypeScriptController {
  text = "Please click the button";

  constructor() {}

  buttonClick () {
    this.text = "The button was clicked";
  }
}

And binding the controller to the DOM using

<main ng-controller="TypeScriptController as TSCtrl">

If I implement this using the standard ES5 function style it works fine (see first half of code snippet below). However the class version does not. Now I can pass in the $scope variable to the controller and bind a property of $scope to this but then the controllerAs syntax in the HTML is ignored. However, I'd like to avoid passing $scope to every controller.

I know that AngularJS 1.3 introduced a bindToController option for directives but I don't see how that could be applied in this context.

Example:

In this example showing the ES5 and TypeScript implementation of a controller. The controller simply contains a method that the ng-click calls to write text below the button. The ES5 version works. The TypeScript version does not. I've also implemented this in a Plunker

angular.module('testApp', [])
  .controller('MainController', MainController);

function MainController() {
  this.text = "Please click the button";
  this.buttonClick = function() {
    this.text = "The button was clicked";
  };
};

// Compiled from TypeScript source
angular.module('testApp').controller('TypeScriptController', function() {
  return new TypeScriptController();
});
// Controller implementation via TypeScript
var TypeScriptController = (function() {
  function TypeScriptController() {
    this.text = "Please click the button";
  }
  TypeScriptController.prototype.buttonClick = function() {
    this.text = "The button was clicked";
  };
  return TypeScriptController;
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>

<body ng-app="testApp">
  <main ng-controller="MainController as mainCtrl">
    <h1>Hello World!</h1>
    <p>
      <input type="button" name="testInput" value="Test button" ng-click="mainCtrl.buttonClick()">
    </p>
    <p>{{mainCtrl.text}}</p>
  </main>
  <main ng-controller="TypeScriptController as TSCtrl">
    <h1>Hello TypeScript!</h1>
    <p>
      <input type="button" name="testInput" value="Test button" ng-click="TSCtrl.buttonClick()">
    </p>
    <p>{{TSCtrl.text}}</p>
  </main>
</body>
Chic
  • 9,836
  • 4
  • 33
  • 62

3 Answers3

3

If you want buttonClick to be an instance member (as you did in the ES5 version) instead of a prototype member, use an arrow function:

// Controller implementation via TypeScript
class TypeScriptController {
  text = "Please click the button";

  constructor() {}

  // Arrow function captures 'this'
  buttonClick = () => {
    this.text = "The button was clicked";
  }
}
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
2

angular.module('testApp') .controller('TypeScriptController', () => new TypeScriptController())

You have this wrong for controllerAs syntax. Instead do:

angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)

And angular will run the new when it sees controller as.

basarat
  • 261,912
  • 58
  • 460
  • 511
  • 1
    Thanks for the tip. For this to work **the controller needs to be passed to angular after the class definition**. This is because the `TypeScriptController` class is defined as a variable that a function is assigned to. Therefore the `TypeScriptController` function is not available until after the class definition. For me, this will make the code passing the controller harder to find (at the end of the file) but it works. – Chic Apr 02 '15 at 15:38
  • I've written [my own answer](http://stackoverflow.com/a/29417325/4252741) incorporating your note and my own realization. – Chic Apr 02 '15 at 15:58
1

As noted by @basarat, the TypeScriptController should be passed directly to the controller.

angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)

The second issue is that the class definition needs to happen before the TypeScriptController is passed to the angular .controller() function. This is because TypeScript compiles classes to variables that contain a function.

// Compiled from TypeScript source
// Note the variable definition
var TypeScriptController = (function() {
  function TypeScriptController() {
    this.text = "Please click the button";
  }
  TypeScriptController.prototype.buttonClick = function() {
    this.text = "The button was clicked";
  };
  return TypeScriptController;
})();

This variable needs to be computed before it can be passed to angular. This differs from a typical JavaScript function definition which is available at the start of the program because of JavaScript hoisting.

// MainController function is already defined
angular.module('testApp', [])
.controller('MainController', MainController);

// Available above because defined as "function FuncName()"
function MainController() {
  this.text = "Please click the button";
  this.buttonClick = function() {
    this.text = "The button was clicked";
  };
}

Therefore the fix is to define the TypeScript class before passing the controller function.

// Controller implementation via TypeScript
class TypeScriptController {
  text = "Please click the button";

  constructor() {}

  buttonClick() {
    this.text = "The button was clicked";
  }
}
// Now that class is defined we can pass the controller directly
angular.module('testApp')
.controller('TypeScriptController', TypeScriptController)
Community
  • 1
  • 1
Chic
  • 9,836
  • 4
  • 33
  • 62