Sunday, August 3, 2014

No Need to $Watch AngularJS “Controller As”

In a previous blog post I discussed how to use Angular’s extend to improve code quality and reusability. In my example I used the new controller as syntax. I see a lot of discussion online about this approach. I like it because it allows you to treat your controllers as pure JavaScript objects rather than glorified grab bags that do nothing but manipulate $scope. The biggest complaint I read is that you still have to take a dependency on $scope when you want to watch properties. Or, do you?

From lessons learned working on a large Angular project (by “large” I mean a team of 25+ developers distributed around the world, with a code base featuring 80,000+ lines of TypeScript code with hundreds of controllers, filters, and services) one mistake I made early on was depending too much on scope. For example, I might assume a detail page inherited the master page’s scope. This dependency on the hierarchy made it difficult to refactor pages, so I quickly learned that communication of properties should take place via components and services and not be based on implied scope.

Watches are similar. You have to ask yourself what you are watching for, then decide if using an actual $watch is worth it. A $watch introduces significant overhead and fires every digest loop. It will be called multiple times when other code mutates the data model, so you want to conserve your watches as much as possible. How can do you do this?

One example I covered in my Angular Debugging and Performance video. The scenario is a paged list. I have one million items in the list and am showing a few per page. You likely know from past experience with paging that there are several variables to calculate, such as how many total pages exist and where a “page” fits into the range of indexed items that represent that page. To handle the paging I created a controller that keeps track of the full list, then exposes a “display list” with the current page. Here is the basic definition with the code to generate the million entries:

function Controller() {
    var idx = 0;
    this.list = [];
    this.displayList = [];
    while (idx < 1000000) {
        this.list.push(Math.random());
        idx += 1;
    }
    this.refreshPages();
}

The controller keeps track of several variables, and they are all recomputed in the refreshPages function that should fire any time the current page changes. Here is the initial definition of the controller and that method:

angular.extend(Controller.prototype, {
    pageSize: 20,
    _currentPage: 1,
    nextPage: 2,
    previousPage: 0,
    currentIndex: 0,
    totalPages: 0,
    refreshPages: function () {
        var curIdx = this.currentIndex;
        this.totalPages = Math.ceil(this.list.length / this.pageSize);
        this.currentIndex = this.pageSize * (this._currentPage - 1);
        this.previousPage = this._currentPage - 1;
        this.nextPage = this._currentPage + 1;
        if (curIdx !== this.currentIndex || this.displayList.length === 0) {
            while (this.displayList.length > 0) {
                this.displayList.pop();
            }
            for (curIdx = this.currentIndex;
                 curIdx < this.list.length && curIdx < this.currentIndex + this.pageSize;
                 curIdx += 1) {
                this.displayList.push(this.list[curIdx]);
            }
        }
    }
});

You may notice the current page is defined as a private property. Why not expose it and then use a $watch? The answer is simple. I control my model, so I don’t need to watch it to know when it is mutated. Instead, I can take advantage of pure JavaScript and expose the current page as a property that updates the necessary variables whenever it changes. After that happens, I quickly clear out the exposed array for the page and repopulate it based on the newly computed variables. Here is the solution that doesn’t involve adding an unnecessary watch but instead uses Object.defineProperty:

Object.defineProperty(Controller.prototype, "currentPage", {
    enumerable: true,
    configurable: true,
    get: function () { return this._currentPage; },
    set: function (val) {
        this._currentPage = val;
        this.refreshPages();
    }
});

Now the property is defined on the controller using pure JavaScript, and can be bound like any other property. Here is markup for the button to navigate to the previous page. It simply decrements the currentPage property and is disabled when you are on the first page.

<button data-ng-click="ctrl.currentPage = ctrl.currentPage-1"
          data-ng-disabled="ctrl.currentPage == 1">
    &lt;
</
button>

If I wanted to optimize even further, I could add an additional condition to ensure the value is actually different before refreshing the pages. The result is a huge list paged efficiently using “controller as” syntax. You can see the running example here: long paged list in AngularJS. The full source is here: long paged list in AngualrJS source code.

The bottom line: I prefer the “controller as” syntax because it allows me to define controllers as pure JavaScript objects with minimal dependencies. I don’t have to explicitly concern myself with $scope and data-binding, and instead can treat the controller itself as a view model and know the exposed properties are available for binding.

Instead of using $watch I use built-in JavaScript features to manage my model. This has the added advantage of making the component easier to test (in this example, you can test the controller without any dependency on Angular whatsoever). If I am concerned about the value changing from another controller, I set up a service for communication rather than adding the watch and again maintain control over the changes without the overhead of being called every digest loop.

$watch my latest video to learn more about AngularJS Debugging and Performance.