Reputation: 3281
I am attempting change the way I am writing AngularJS apps from a plain-javascript to using TypeScript as a pre-processor.
I am struggling to reconcile the two approaches when it comes to scoped method calls.
For illustrative purposes, let's consider the common menu use-case; I wish to highlight a specific menu item which is currently displayed. The HTML template looks like this:
<ul class="nav navbar-nav">
...
<li ng-class="{active: isSelected('/page1')}"><a href="#/page1">Page 1</a></li>
...
</ul>
This anticipates a scoped function called isSelected
. With old-school javascript coding, I' write this as:
$scope.isSelected = function(path) {
return $location.path().substr(0, path.length) == path;
}
This anonymous function declaration doesn't really seem to honor the more traditional class model of TypeScript. In typescript, I find myself tempted to write this:
export interface MenuScope extends ng.IScope {
isSelected(path: String): boolean;
}
export class MenuController {
location: ng.ILocationService;
scope: MenuScope;
constructor($scope: MenuScope, $location: ng.ILocationService) {
this.scope = $scope;
this.location = $location;
this.scope.isSelected = function(path) { return this.isSelected(path) }.bind(this);
}
isSelected(path: String): boolean {
return this.location.path().substr(0, path.length) == path;
}
}
In this case, isSelected
belongs to the controller, rather than the scope. This seems sensible. However, the "link" between the scope and controller still relies on an anonymous method.
Even worse, I've had to explicitly bind the context of this
to ensure I can write this.location
to access the location service in the implementation of isSelected()
.
One of the benefits I am looking for from TypeScript is a clearer way of writing code. This indirection through a binded anonymous function seems to be the antithesis of this.
Upvotes: 2
Views: 857
Reputation: 8146
Here is a simple app with a controller and a service. I use this style in my projects:
/// <reference path="typings/angularjs/angular.d.ts" />
module App {
var app = angular.module("app", []);
app.controller("MainController as vm", Controllers.MainController);
app.service("backend", Services.Backend);
}
module App.Controllers {
export class MainController {
public persons: Models.Person[];
static $inject = ["$location", "backend"];
constructor(private $location: ng.ILocationService, private backend: Services.Backend) {
this.getAllPersons();
}
public isSelected(path: string): boolean {
return this.$location.path().substr(0, path.length) == path;
}
public getAllPersons() {
this.backend.getAllPersons()
.then((persons) => {
this.persons = persons;
})
.catch((reason) => console.log(reason));
}
}
}
module App.Services {
export class Backend {
static $inject = ["$http"];
constructor(private $http: ng.IHttpService) { }
public getAllPersons(): ng.IPromise<Models.Person[]> {
return this.$http.get("api/person")
.then((response) => response.data);
}
}
}
module App.Models {
export interface Person {
id: number;
firstName: string;
lastName: string;
}
}
controller as
syntax. So everything you define in class is accessible through vm
in the view (controller scope). Here we have persons
, isSelected
and getAllPersons
.static $inject
that is a string[]
, then add them as constructor parameters respectively. This role is also usable when defining services and it is minifiable.$scope
to your controller class to access scope specific tools such as $apply
, on
etc.ng.IPromise<Model>
then return response.data
to ensure that type of your return method is just entities, not http related data.Upvotes: 2
Reputation: 6958
We were considering a similar conversion (e.g. from Javascript to Typescript for Angular). There were certain things (like your example) that looked very odd as we started to implement. The quickest way to go from what you have is to use the controller as
syntax. This way, you expose methods directly on the controller.
<!-- use controller as syntax -->
<div ng-controller="MenuController as menu">
<ul class="nav navbar-nav">
...
<li ng-class="{active: menu.isSelected('/page1')}"><a href="#/page1">Page 1</a></li>
...
</ul>
</div>
This would allow you to get past the need to bind the scope's method back to that on the controller. Things I don't like about this approach:
class
for code that still has room for optimization. See generated code for Inheritance at the Typescript Playground (extends function generated for each .js file where you want to extend a class unless you have your references are on point, the prototype of the function could be cached to add methods to it instead of ClassName.prototype.method for each and every method...), but I digressThe other option is to not use classes, but to stick to strongly typed functions:
export function MenuController($scope: MenuScope, $location: ng.ILocationService): any {
$scope.isSelected = function(path:string): boolean {
return $location.path().substr(0, path.length) == path;
}
}
Since angular is responsible for instantiating the controller, the return type any
doesn't matter. But you could get caught if you had a typo on your $scope's method.
To get around this, you can go a step further by using controller as
syntax. The example below won't let you actually new up your controller yourself (e.g. new MenuController($location) would fail with only void function can be called with new keyword
), but this is negligible since angular handles the instantiation for you.
export interface IMenuController {
isSelected(path: string): boolean;
}
export function MenuController($location: ng.ILocationService): IMenuController {
var self:IMenuController = this;
self.isSelected = function(path:string): boolean {
return $location.path().substr(0, path.length) == path;
}
// explicitly return self / this to compile
return self;
}
TL DR: I'm a fan of the compile time checking of types, and would love to use the concept of class. However, I don't think it completely fits with the Angular 1.x model. It seems like this is designed to work for Angular2. Use strongly typed functions instead for Angular 1.x.
Upvotes: 1
Reputation: 771
You shouldn't store your $scope
as a variable in this
but instead use this
as the $scope
, the way to do this is to do $scope.vm = this;
in the beginning of the constructor, now every function and variable of the class will be part of the $scope.
You can't avoid this.location = $location
because this is the syntax of TypeScript.
BTW you should use $inject for the dependencies.
Upvotes: 2