22

I made a directive designed to be attached to an element using the ngModel directive. If the model's value matches something the value should then set to the previous value. In my example I'm looking for "foo", and setting it back to the previous if that's what's typed in.

My unit tests passed fine on this because they're only looking at the model value. However in practice the DOM isn't updated when the "put back" triggers. Our best guess here is that setting old == new prevents a dirty check from happening. I stepped through the $setViewValue method and it appears to be doing what it ought to. However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value. It works fine, but I just want to see if there's a more appropriate way of doing this.

Code is below, here's a fiddle with the same.

angular.module('myDirective', [])
    .directive('myDirective', function () {
    return {
        restrict: 'A',
        terminal: true,
        require: "?ngModel",
        link: function (scope, element, attrs, ngModel) {
            scope.$watch(attrs.ngModel, function (newValue, oldValue) {
                //ngModel.$setViewValue(newValue + "!");   

                if (newValue == "foo")
                {
                    ngModel.$setViewValue(oldValue);   
                    /* 
                        I Need this render call in order to update the input box; is that OK?
                        My best guess is that setting new = old prevents a dirty check which would trigger $render()
                    */
                    ngModel.$render();
                }
            });
        }
    };
});

function x($scope) {
    $scope.test = 'value here';
}
jdp
  • 3,446
  • 2
  • 30
  • 54
David Peters
  • 1,938
  • 1
  • 20
  • 18

1 Answers1

33

Our best guess here is that setting old == new prevents a dirty check from happening

A watcher listener is only called when the value of the expression it's listening to changes. But since you changed the model back to its previous value, it won't get called again because it's like the value hasn't changed at all. But, be careful: changing the value of a property inside a watcher monitoring that same property can lead to an infinite loop.

However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value.

That's correct. $setViewValue sets the model value as if it was updated by the view, but you need to call $render to effectively render the view based on the (new) model value. Check out this discussion for more information.

Finally, I think you should approach your problem a different way. You could use the $parsers property of NgModelController to validate the user input, instead of using a watcher:

link: function (scope, element, attrs, ngModel) {
  if (!ngModel) return;

  ngModel.$parsers.unshift(function(viewValue) {
    if(viewValue === 'foo') {                 
      var currentValue = ngModel.$modelValue;
      ngModel.$setViewValue(currentValue);
      ngModel.$render(); 
      return currentValue;
    }
    else 
      return viewValue;
  });
}

I changed your jsFiddle script to use the code above.

angular.module('myDirective', [])
.directive('myDirective', function () {
  return {
    restrict: 'A',
    terminal: true,
    require: "?ngModel",
    link: function (scope, element, attrs, ngModel) {
      if (!ngModel) return;

      ngModel.$parsers.unshift(function(viewValue) {
        if(viewValue === 'foo') {                 
          var currentValue = ngModel.$modelValue;
          ngModel.$setViewValue(currentValue);
          ngModel.$render(); 
          return currentValue;
        }
        else 
          return viewValue;
      });
    }
  };
});

function x($scope) {
  $scope.test = 'value here';
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<h1>Foo Fighter</h1>
I hate "foo", just try and type it in the box.
<div ng-app="myDirective" ng-controller="x">
  <input type="text" ng-model="test" my-directive>
  <br />
  model: {{test}}
</div>
Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Michael Benford
  • 14,044
  • 3
  • 60
  • 60
  • Thanks, adding a $parser (or a $formatter?) makes much more sense than what I was doing. I'm having trouble unit testing this though; do I have to call an additional Angular method to kick off the parse process? Currently when my unit test manipulates the model value the new parser handler doesn't fire. – David Peters Oct 03 '13 at 21:48
  • `$parser` is used to validate what comes from the view and `$formatter` is used to format the model before it gets rendered. If you want to prevent someone from adding `foo` directly to the model, then using a parser won't work. – Michael Benford Oct 03 '13 at 22:34
  • 1
    About the testing, basically you should trigger an `input` event on the input field where the `ng-model` directive is applied. That will suffice for Angular to run your code. If you need an example, create another question and I'll answer it for you. I'm afraid it'll clutter the comments if I put it in here. – Michael Benford Oct 03 '13 at 22:58
  • 1
    Found your other [answer/fiddle](http://stackoverflow.com/a/17962618/89176) on the topic, works great. Thanks again! – David Peters Oct 04 '13 at 14:20
  • Oh, I didn't remember that question. Glad you found it. Perhaps you'd be interested in checking out [the tests](https://github.com/mbenford/ngTagsInput/blob/master/test/ng-tags-input.spec.js) I had written for the directive I talked about in that post. – Michael Benford Oct 04 '13 at 15:07