Writing Cleaner AngularJS with TypeScript and ControllerAs
Our team made the move to TypeScript and Angular at the tail end of last year. I’d had a look at Angular a year or so ago but struggled to get my head around the excessive usage of $scope and the nesting of $parent items that needed to be traversed.
Since then (with version 1.2.0), Angular now supports Controller As which lets us define properties and methods on the controller as a class rather than in a shared scope object.
It’s easier to see with an example. Here’s a controller for a to-do list using $scope:
This could then be used with a view such as:
There’s nothing badly wrong with this. $scope only really starts to become a problem when you start nesting them.
The above is a very contrived example but you can see where the scope hierarchies start to become problematic.
This is where Controller As comes in.
Controller As
The Controller As syntax allows us to use the controller as an instance and bind to and interract with properties and methods directly on it.
Using our example from above, our to-do list controller can drop all references to $scope and would look something like this:
Note the change (following convention) from toDoListController to ToDoListController as we are now treating the controller as a class.
Our view can now be changed to this:
This is a little more verbose than using $scope everywhere but is more explicit with references. Have a look at our contrived example to see just how much more readible it becomes:
That’s the first piece of our structure.
TypeScript
As written on the TypeScript site:
TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
Any browser. Any host. Any OS. Open Source.
One of the main benfits from using TypeScipt is that it allows you to use ES6 features such as Classes right now.
ES5 ‘Class’
TypeScript Class
As we saw above, using Controller As allows us to access the Controller as an instance. With TypeScript, we can make that instance an explicit class.
Now we have an overview of how this works, let’s build out a simple To-Do app and see how everything fits together.
The To-Do Angular/TypeScript App
To start with, we’re going to need to set out a file structure. The scripts folder in a Visual Studio solution quickly becomes a no-man’s-land of libraries from NuGet, so we’ll stay out of there. Under the root, we’ll create an app folder and our files will live under there:
First of all we’ll need a model for a To-Do item to appear in the list, so we’ll create that under the ./models folder. TypeScript also gives us support for module definition (very similar to namespacing in the .NET world) so we’ll use that to keep from polluting the global namespace.
Modules also make registering components with Angular a lot simpler, but we’ll cover that later.
models/list-item.ts
Here we’re passing the task name through the constructor and also setting the isComplete flag (which defaults to false).
We could manage our to-do lists directly on the page using controller instances but, to make them more easily re-usable, let’s wrap them up as an Angular Directive.
For our to-do list, we want to be able to pass in a name for the list as we create it on the page. We’ll wrap this up in an interface matching the scope definition (line 10) then use that with the directive’s controller.
Each directive can have its own controller instance and its own isolate scope. The isolate scope (as it sounds) keeps everything in the directive scope isolated from the main app scope. Although we’re not going to be using the $scope directly, we still need to use the directive’s scope binding to pull in data from its usage on the page (passing in the list name as an HTML attribute).
directives/to-do-list.ts
In line 13 we use the Controller As syntax to give our controller instance an alias for use with the template defined on line 14.
Our controller (referenced on line 12) is very similar to the example code earlier:
controllers/to-do-list-controller.ts
So that the file will still function when compressed, we use the static $inject method on line 9 to specify what parameter Angular should inject in the class constructor. This instance (line 12) is the isolate scope of the directive.
The last part of our directive is the view. This uses the controller alias from our directive definition and lists the to-do items along with a form for adding new ones:
views/to-do-list.html
All references to our controller are made using the vm alias (vm = view model).
We now only need our main.ts file and our HTML page to pull everything together:
main.ts
Since we’re using modules, we only need to reference the module name rather than each individual item when registering our Angular components. This can greatly simplify a large app as we can maintain a structure of one class per file while not having to update the app definition every time a new one is added. By using the module in the registration, all items within that module will be registered at once.
default.html
Testing
Since we have no dependency on $scope, testing of our controller becomes very easy since we no longer need to pull in any references to Angular at all. Our controller is simply a class that is expecting something that implements the IToDoListScope interface in the constructor. So this becomes easy to mock.
Our full tests for the controller (using Jasmine) then look like this:
And that’s about it. This is still an evolving project so we may discover better ways to structure or organise some of the code here. If you have any suggested improvements, please let me know.
All code used above is available on GitHub.