stianlp
stianlp

Reputation: 1019

AngularJS : $watch vs. $observe when newValue === oldValue

If this is already explained or discussed somewhere, I am very sorry, but I couldn't find this exact problem discussed anywhere.

So I have an angular directive with one data binding 'myvar' (= or @ makes no difference). The value from the data binding is used in the directive: scope.myvarStr = scope.myvar + 'somestring'. Then I bind myvarStr in the template.

Because scope.myvarStr must be modified when scope.myvar changes, I used $watch('myvar', function(...)) to watch the value and update scope.myVarStr when needed. In the watch function I put the classic if (newValue === oldValue) return;

The problems started the very first time $watch fired and the two values were equal; then the view was not updated. I could easily see that from console.log(scope.myvar) on the first line in the link function that scope.myvar was undefined (or '' dependent on binding type) to begin with and that the value had changed to something else when I did a console.log in the $watch.

I googled for an hour or so, and found this: https://github.com/angular/angular.js/issues/11565 However, this issue wasn't discussed anywhere else, so I looked googled more and came across $observe AngularJS : Difference between the $observe and $watch methods

When I changed from $watch to $observe, all my problems went away and I can still use if(newValue === oldValue) return;.

(function(directives) {
'use strict';

directives.directive('someDir', [function() {
    return {
        restrict: 'E',
        scope: {
            myvar: '=' //or @ didn't matter at the time...
        },
        template: '<p>{{myvarStr}}</p>',
        link: function(scope, el, attrs) {

            function toString() {
                if (scope.myvar < 1000) {
                    scope.myvarStr = scope.myvar;
                } else {
                    scope.myvarStr = scope.myvar/1000 + 'k';
                }
            }
            toString();

            scope.$watch('myvar', function(newValue, oldValue) {
                console.log("changed", newValue, oldValue)
                if (newValue == oldValue) return;
                toString();
            },true);

            // When changing to $observe it works :)
            //attrs.$observe('myvar', function(newValue, oldValue) {
            //    console.log("changed", newValue, oldValue)
            //    if (newValue == oldValue) return;
            //    toString();
            //},true);

        }
    };
}]);
}(angular.module('myApp.directives')));

Suggestion: As far as I understand this issue occurs with $watch because the scope value is never changed. It takes a while for the directive to pick up the value, until then the binding is just an empty string or something and when the value is detected the $watch fires but the actual scope value has not changed (or as explained in the first link; the first watch fires when the value 'appears' in the directive).

Upvotes: 4

Views: 4098

Answers (1)

gkalpak
gkalpak

Reputation: 48212

I don't quite understand your suggestion/explanation, but I feel like things are much simpler than you make it appear.

You don't need the newValue === oldValue test, because your watch-action is idempotent and cheap. But even if you do, it only means you need to initialize the value yourself (e.g. by calling toString() manually), which you seem to be doing and thus your directive should work as expected. (In fact, I couldn't reproduce the problem you mention with your code.)

Anyway, here is a (much simpler) working version:

.directive('test', function testDirective() {
  return {
    restrict: 'E',
    template: '<p>{{ strVal }}</p>',
    scope: {
      val: '='
    },
    link: function testPostLink(scope) {
      scope.$watch('val', function prettify(newVal) {
        scope.strVal = (!newVal || (newVal < 1000)) ?
            newVal : 
            (newVal / 1000) + 'K';
      });
    }
  };
})

BTW, since you are only trying to "format" some value for display, it seems like a filter is more appropriate (and clear imo), than a $watch (see the above demo):

.filter('prettyNum', prettyNumFilter)
.directive('test', testDirective)

function prettyNumFilter() {
  return function prettyNum(input) {
    return (!input || (input < 1000)) ? input : (input / 1000) + 'K';
  };
}

function testDirective() {
  return {
    template: '<p>{{ val | prettyNum }}</p>',
    scope: {val: '='}
  };
}

Upvotes: 0

Related Questions