AngularJS + TypeScript – How To Setup a Watch (And 2 Ways to Do it Wrong)
Join the DZone community and get the full member experience.
Join For FreeIntroduction
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 an scope object or part of that changes. I have found 4 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, (oldValue: string, newValue: 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 and 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, (oldValue: string, newValue: 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.
Published at DZone with permission of Joost van Schaik, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments