Using $scope.$watch with Angular ES6/ES2015 class syntax

I recently started using more and more of ES6 classes in Angular code and haven't really hit any scenario where I couldn't figure out the syntax right away, until today. This post will be about a small trick you have to do to be able to use $watch within a controller that is using class syntax.

Let's assume we want to declare a regular Angular controller with ES6's class. That is described in lots of places (like this one) but for convenience sake will repeat it here. Let assume we have our controller looking something like this:

'use strict';

import angular from 'angular';

class WatchController {  
    constructor () {
        this.coolNumber = 42;
    }
}

angular  
    .module('sample')
    .controller('WatchController', WatchController);

export { WatchController };  

So far so good, we use our nice coolNumber variable somewhere on the view, but suddenly we have a need to track how it's changed from some place outside the controller. The most convenient (but not efficient) way to track modification of the variable is though $scope.$watch(). Let's try and add that to our code.

First of all, we are most likely using ControllerAs syntax to be able to use class syntax and now we need to include $scope in our controller all of the sudden. So now our controller would look something like this:

class WatchController {  
    constructor ($scope) {
        this.coolNumber = 42;
        this.$scope = $scope;
    }
}

WatchController.$inject = ['$scope'];  

Yeah, that this.$scope assignment is pretty ugly, but it's the only way that we could use $scope inside other class methods.

Let's say we want to call some other service whenever the coolNumber changes. So, we introduce the watch a way that is probably the most intuitive, like so:

class WatchController {  
    constructor ($scope, CoolNumberLovingService) {
        this.coolNumber = 42;
        this.$scope = $scope;
        this.numberLover = CoolNumberLovingService;
        this.$scope.$watch('coolNumber', this.coolNumberChanged);
    }

    coolNumberChanged (newValue, oldValue) {
        this.numberLover.itHasChanged(newValue, oldValue);
    }
}

WatchController.$inject = ['$scope', 'CoolNumberLovingService'];  

We fire up the tests, or if we haven't written them just try things out in the browser, but nothing works. The console is full of these errors: TypeError: this.numberLover is not a function. And you'd be like:

confused

No worries, to make it work we have to do this one trick - make use of ES6's fat arrow notation to have our scope bound to proper context:

class WatchController {  
    constructor ($scope, CoolNumberLovingService) {
        this.coolNumber = 42;
        this.$scope = $scope;
        this.numberLover = CoolNumberLovingService;
        this.$scope.$watch('coolNumber', this.coolNumberChanged());
    }

    coolNumberChanged (newValue, oldValue) {
        return () => {
            this.numberLover.itHasChanged(newValue, oldValue);
        };
    }
}

The whole trick is in that returned anonymous function declared using a fat arrow. It lexically binds this to our WatchController and that way doesn't mess up scopes anymore. You can read about it a bit more on MDN

That's all folks! Hope it saves some time for poor soles that are still lost in this scope binding although using all the coolest ES6 bells and whistles.

Special thanks to my friend Tomek for helping figure this thing quickly

Cover image by Wilson Hui