Ditch Your Angular 1.x Watchers! Quick-Swap Them for Observables

Converting $watch to observeOnScope

Note: I wrote this article to help my teammates understand my quest to eradicate $watch in our application. I hope it helps someone else, too!

Observables are very powerful and have a lot of different functionality, but they can also be very simple to use. Here, we will convert a few $watch's to observeOnScope.

observeOnScope uses $watch in the background, but switching will gain you access to the wide world of Observers and will start to move you down the future path where Angular 2 has switched to exclusively using Observables for change detection.


The rx.angular.js Library

RXJS provides a helper library to use Observables on Angular 1.x projects.

Here is the documentation.

For this discussion, we'll only be using the observeOnScope function. You give it a $scope and an attribute and it creates an Observable with them.

Begin by adding rx.angular to your bower files.

bower install angular-rx --save  

Then add the files to your html.

<script src="bower_components/rxjs/modules/rx-lite/rx.lite.js"></script>  
<script src="bower_components/rx-angular/modules/rx.lite.angular/rx.lite.angular.js"></script>  
<script src="bower_components/angular/angular.js"></script>  

Tip: Make sure you import rx.angular.js before angular.js or it gets mad.

Note: The documentation doesn't say you need to import rxjs, but I had to.

Lastly, we need to declare RXJS as a dependency to our app.

angular.module('app', [  
  ...
  'rx',
  ...
]);

Converting Basic Watchers


Non-"controllerAs" controller

We'll start by looking at a controller that's directly using the $scope object. Here's its $watch:

Controller1.$inject = ['$scope'];  
function Controller1($scope) {  
    $scope.property1 = false;

    $scope.$watch('property1', function(newValue, oldValue) {

        if (newValue && oldValue) {
            //Do Something        
        }
    });
}

$watch expects the property name and the function that is to be run when the property changes.

Similarly, you'll need both when converting to observeOnScope.

It expects the following arguments:

  • scope - the scope we're working on
  • propertyName or function - the property name, or a function that returns the value to be checked. (The function will be called with the provided scope as its parameter.)

First, you'll need to declare observeOnScope as a dependency of the controller.

Controller1.$inject = ['$scope', 'observeOnScope'];  
function Controller1($scope, observeOnScope) {  
    ...
}

Then we define an Observable on the thing we were $watching. That definition would look like this:

observeOnScope($scope, 'property1');  

But you still need to tell the application what to do when the Observable changes. That is accomplished with a "subscription."

Subscriptions are functions that are called when the Observable changes. When fired, they are given an object that has the attributes newValue and oldValue.

observeOnScope($scope, 'property1')  
    .subscribe(function(change) {

        if (change.newValue && change.oldValue) {
            //Do Something        
        }
    });

That's it! You're done!

Here's what the controller looks like now:

Controller1.$inject = ['$scope', 'observeOnScope'];  
function Controller1($scope, observeOnScope) {  
    $scope.property1 = false;

    observeOnScope($scope, 'property1')
        .subscribe(function(change) {

            if (change.newValue && change.oldValue) {
                //Do Something        
            }
        });
}


Converting a controllerAs Controller

This conversion follows the same formula as described above, but you need to be careful to provide the right attribute name (just like you have to be careful with a $watch)

// controllerAs: 'ctrlAs'
ControllerAs1.$inject = ['$scope'];  
function ControllerAs1($scope) {  
    var vm = this;

    vm.property1 = false;

    $scope.$watch('ctrlAs.property1', function(newValue, oldValue) {

        if (newValue && oldValue) {
            //Do Something        
        }
    });
}

Becomes...

// controllerAs: 'ctrlAs'
ControllerAs1.$inject = ['$scope', 'observeOnScope'];  
function ControllerAs1($scope, observeOnScope) {  
    var vm = this;

    vm.property1 = false;

    observeOnScope($scope, 'ctrlAs.property1')
        .subscribe(function(change) {

            if (change.newValue && change.oldValue) {
                //Do Something        
            }
        });
}

Observing an Object's Attributes

Sometimes we need to detect changes on attributes of an Object. With $watch we could pass in the optional "deep" boolean parameter, like this:

$scope.object1 = {...};

$scope.$watch('object1', function(){}, true);

Similarly, you can tell observeOnScope to detect "deep" attributes changes. Like this:

$scope.object1 = {...};

observeOnScope($scope, 'object1', true)  
    .subscribe(function(){});

Unsubscribing from an Observable

Sometimes you need to cancel a $watch.

The $watch function returns a function that cancels itself. So you'd cancel it like this:

// controllerAs: 'ctrlAs'
ControllerAs1.$inject = ['$scope'];  
function ControllerAs1($scope) {  
    var vm = this;

    vm.property1 = false;

    vm.cancelWatcher = $scope.$watch('ctrlAs.property1', function(newValue, oldValue) {

        if (newValue && oldValue) {
            //Do Something        
        }
    });

    vm.cancelWatcher();
}

observeOnScope provides a similar feature, but it is a bit more nuanced.

Here's the code, followed by a bit of a technical discussion on what is happening (and why it's useful).

// controllerAs: 'ctrlAs'
ControllerAs1.$inject = ['$scope', 'observeOnScope'];  
function ControllerAs1($scope, observeOnScope) {  
    var vm = this;

    vm.property1 = false;

    vm.propertySubscription = observeOnScope($scope, 'ctrlAs.property1')
        .subscribe(function(change) {

            if (change.newValue && change.oldValue) {
                //Do Something        
            }
        });

    vm.propertySubscription.dispose();
}

This has the same effect as canceling a $watch, in that the function you provide to the subscription will cease to be called when the property changes. However, it has some subtle differences.

The result of calling .subscribe() is what is called a Disposable. This simply means that it's an object that can control whether or not it is active or stopped.

With rx.angular.js when a $scope is destroyed, all subscriptions to it are disposed of by default.

It's important to remember that there are two parts to the change detection now: the Observable and the subscriptions to it.

This allows you to provide an observable from a service, and let different parts of the application control their own subscriptions. So, disposing of a subscription in one controller will not affect the Observable or any other subscriptions to it.


Using a Function to Provide Subject of Observable

$watch can accept a function that returns the value you want to watch. For example:

// controllerAs: 'ctrlAs'
Controller1.$inject = ['$scope'];  
function Controller1($scope) {  
    $scope.property1 = 1;
    $scope.property2 = 2;

    $scope.$watch(function() {
            return $scope.property1 + $scope.property2;
        }, function(newValue, oldValue) {

            if (newValue && oldValue) {
                //Do Something        
            }
        });
}

observeOnScope allows you to pass in a function as well. It will even pass in the scope you provided in the first parameter to this function.

Controller1.$inject = ['$scope', 'observeOnScope'];  
function Controller1($scope, observeOnScope) {  
    $scope.property1 = 1;
    $scope.property2 = 2;

    observeOnScope($scope, function(scope) {
            return scope.property1 + scope.property2;
        })
        .subscribe(function(change) {

            if (change.newValue && change.oldValue) {
                //Do Something        
            }
        });
}

Notice we're using scope.property1 instead of $scope.property1 inside the "subject" function. While not always necessary, this can be really helpful if, for some reason, you need to pass in a different scope than the $scope that you're defining your Observable in. It is "best practice" to limit the scope of this "subject" function to its own closure.


Clean It All Up With Named Functions

If you're feeling feisty...

Controller1.$inject = ['$scope', 'observeOnScope'];  
function Controller1($scope, observeOnScope) {  
    $scope.property1 = 1;
    $scope.property2 = 2;

    observeOnScope($scope, getAdditionResult)
        .subscribe(onValueChange);

    ////////////////

    function getAdditionResult(scope) {
        return scope.property1 + scope.property2;
    }

    function onValueChange(change) {

        if (change.newValue && change.oldValue) {
            //Do Something        
        }
    }
}

Final Thoughts

This article barely scratches the surface of how you can use Observables, but it should give you the gist of how to do a quick 1-for-1 swap with $watch. Enjoy.


comments powered by Disqus