4

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).

Community
  • 1
  • 1
stianlp
  • 999
  • 9
  • 19
  • This seems like a new change to me. I was sure that watch used to return null or undefined for the old value on run.... Testing that the "old value" equals the new value seems really silly to me. If the old value is undefined then skip makes much more sense. Better yet, have a $watch option to not run first time. – toxaq Oct 29 '15 at 07:12

1 Answers1

0

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: '='}
  };
}
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • Well, the question is actually about the fact that one use $watch with if(newValue === oldValue) return; There is no guarantee that the toString() function will ever be called. If toString isn't called with the correct value, then the view will not be updated. Btw, a filter is super smart. Haven't used those too much before, but they might come in handy here! – stianlp May 21 '15 at 11:13
  • I still don't get the question. If you don't call `toString()` when `newValue === oldValue` and the value never changes, then why is it unexpected that `toString()` is never called ? – gkalpak May 21 '15 at 11:38
  • It is not unexpected. The problem is that when the DOM renders, scope.myvar is an empty string inside the directive. When the The $watch fires for the first time, the two values are equal (let's say 1400 === 1400). So the value will never be updated – stianlp May 21 '15 at 11:43
  • @stianlp: I can't reproduce the problem you refer to. Could you post a reproduction ? – gkalpak May 21 '15 at 21:59