AngularJS + TypeScript – how to setup a watch (and 2 ways to do it wrong)

3 minute read

Introduction

After setting up my initial application as described in my previous post, I went about to set up a watch. For those who don’t know what that is – it’s basically a function that gets triggered when a scope object or part of that changes. I have found 3 ways to set it up, and only one seems to be (completely) right.

In JavaScript, you would set up a watch like this sample I nicked from Stack Overflow:

function MyController($scope) {
   $scope.myVar = 1;

   $scope.$watch('myVar', function() {
       alert('hey, myVar has changed!');
   });
   $scope.buttonClicked = function() {
      $scope.myVar = 2; // This will trigger $watch expression to kick in
   };
}

So how would you go about in TypeScript? Turns out there are a couple of ways that compile but don’t work, partially work, or have unexpected side effects.

For my demonstration, I am going to use the DemoController that I made in my previous post.

Incorrect method #1 – 1:1 translation.

/// <reference path="../scope/idemoscope.ts" />
/// <reference path="../scope/person.ts" />
module App.Controllers {
    "use strict";
    export class DemoController {

        static $inject = ["$scope"];

        constructor(private $scope: Scope.IDemoScope) {
            if (this.$scope.person === null || this.$scope.person === undefined) {
                this.$scope.person = new Scope.Person();
            }
            this.$scope.$watch(this.$scope.person.firstName, () => {
                alert("person.firstName changed to " +
                    this.$scope.person.firstName);
            });
        }

        public clear(): void {
            this.$scope.person.firstName = "";
            this.$scope.person.lastName = "";
        }
    }
} 

The new part is in red. Very cool – we even use the inline ‘delegate-like’ notation do define the handler inline. This seems plausible, but does not work. What it does is, on startup, give the message “person.firstName changed to undefined” and then it never, ever does anything again. I have spent quite some time looking at this. Don’t do the same – read on.

Incorrect method #2 – not catching the first call

To fix the problem above, you need to use the delegate notation at the start as well:

this.$scope.$watch(() => this.$scope.person.firstName, () => {
    alert("person.firstName changed to " +
        this.$scope.person.firstName);
});

See the difference? As you now type a “J” in the top text box, you immediately get a “person.firstName changed to J” alert. Making it almost impossible to type. But you get the drift.

But then we arrive at the next problem – this is still not correct: it goes off initially, when nothing has changed yet. This is undesirable in most occasions.

The correct way

It appears the callback actually has a few overloads with a couple of parameters, of which I usually only use oldValue and newValue to detect a real change. Kinda like you do in an INotifyPropertyChanged property:

this.$scope.$watch(() => this.$scope.person.firstName, 
                         (newValue: string, oldValue: string) => {
    if (oldValue !== newValue) {
        alert("person.firstName changed to " +
            this.$scope.person.firstName);
    }
});

Now it only goes off when there’s a real change in the watched property.

…and possibly an even better way

I am not really a fan of a lambda calling a lambda in a method call, so I would most probably refactor this to

constructor(private $scope: Scope.IDemoScope) {
    if (this.$scope.person === null || this.$scope.person === undefined) {
        this.$scope.person = new Scope.Person();
    }
    this.$scope.$watch(() => this.$scope.person.firstName, 
                            (newValue: string, oldValue: string) => {
        this.tellmeItChanged(oldValue, newValue);
    });
}

private tellmeItChanged(oldValue: string, newValue: string) {
    if (oldValue !== newValue) {
        alert("person.firstName changed to " +
            this.$scope.person.firstName);
    }
}

as I think this is just a bit more readable, especially if you are going to do more complex things in the callback.

Demo solution can be found here

Update 27-02-2015: in the original post I swapped oldValue and newValue. No-one apparently even caught that, until my colleague Adrian Tudorache actually tried to follow this post. Thanks Adrian!