AngularJS - In a directive that changes the model value, why do I have to call $render?

27,683

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>
Share:
27,683

Related videos on Youtube

David Peters
Author by

David Peters

Homebrewer, kayaker, developer.

Updated on December 30, 2020

Comments

  • David Peters
    David Peters over 3 years

    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';
    }
    
  • David Peters
    David Peters over 10 years
    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.
  • Michael Benford
    Michael Benford over 10 years
    $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
    Michael Benford over 10 years
    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.
  • David Peters
    David Peters over 10 years
    Found your other answer/fiddle on the topic, works great. Thanks again!
  • Michael Benford
    Michael Benford over 10 years
    Oh, I didn't remember that question. Glad you found it. Perhaps you'd be interested in checking out the tests I had written for the directive I talked about in that post.