angularjsangularjs-directiveangularjs-ng-modelangularjs-forms

Angularjs form $error is not getting updated when the model is updated inside a directive unless the model be cleared


I can't figure out what's happening in the following example. I just trying to create my own required validation in my own directive where I have an array and I want to make it required (it's a simplification of what I want to do but enough to show the point)

Fiddler: http://jsfiddle.net/gsubiran/p3zxkqwe/3/

angular.module('myApp', [])

.directive('myDirective', function($timeout) {
    return {
      restrict: 'EA',
      require: 'ngModel',
      controller: 'myDirectiveController',
      controllerAs: 'D_MD',
      link: function(scope, element, attrs, ngModel) {
        ngModel.$validators.required = function(modelValue) {
          var result = false;
          if (modelValue && modelValue.length > 0)
            result = true;

          return result;
        };
      },
      bindToController: {
        ngModel: '='
      },
      template: '(<span>ArrayLength:{{D_MD.ngModel.length}}</span>)<br /><input type=button value="add (inside directive)" ng-click=D_MD.AddElem() /><br /><input value="clear (inside directive)" type=button ng-click=D_MD.Clear() />'
    };   })   .controller('myDirectiveController', [function() {
    var CTX = this;
    //debugger;
    //CTX.ngModel = "pipo";
    CTX.clearModel = function() {
      CTX.ngModel = [];
    };
    CTX.AddElem = function() {
      CTX.ngModel.push({
        Name: 'obj100',
        Value: 100
      });
    };
    CTX.Clear = function() {
      CTX.ngModel = [];
    };   }])   .controller('MainCtrl', function($scope) {
    var CTX = this;
    CTX.patito = 'donde esta el patito';
    CTX.arrayElements = [];
    CTX.setElements = function() {
      CTX.arrayElements = [{
        Name: 'obj0',
        Value: 0
      }, {
        Name: 'obj1',
        Value: 1
      }, {
        Name: 'obj2',
        Value: 2
      }];
    };
    CTX.clearElements = function() {
      CTX.arrayElements = [];
    };   })

When I hit the add (outside directive) button, the required works fine, but when I hit the add (inside directive) button I still getting the required error in the form (the form is defined outside directive).

But the more confusing thing for me is the following:

When I hit the clear (inside directive) button after hitting add (outside directive) button to make the required error go out, in this case the form is updating and the validation error is showing.

Why $validations.required is not firing inside the directive when I add new element to array but yes when I clear it?

Any ideas?

******* UPDATE *******

It seems to be related with array.push if I change array.push with the assignation of new array with wanted elements inside it works ok. Still the question why it is happening.

As workaround I changed in the directive the AddElem function in this way:

CTX.AddElem = function() {
     CTX.ngModel = CTX.ngModel.concat({
        Name: 'obj100',
        Value: 100
      });
    };

Solution

  • The ngModel you use here is a JS object. Angular has a reference to that object in its $modelValue and $viewValue (because angular basically does $viewValue = $modelValue). The $modelValue is the actual value of the ngModel, which, if you change it, will change the $viewValue after having run $validators.

    To know if your ngModel has Changed, angular compares the ngModel.$viewValue with the ngModel.$modelValue. Here, you are doing a push() to the $viewValue which is updating the $modelValue at the same time because they are just references of each other. Therefore, when comparing them, they have the same value ! Which is why angular does not run your $validator.

    The docs explain it :

    Since ng-model does not do a deep watch, $render() is only invoked if the values of $modelValue and $viewValue are actually different from their previous values. If $modelValue or $viewValue are objects (rather than a string or number) then $render() will not be invoked if you only change a property on the objects.

    If I over-simplify angular code, this snippet explains it:

    var myArray = [];
    
    var ngModel = {
      $viewValue: myArray,
      $modelValue: myArray,
      $validate: function () { console.log('validators updated'); }, // log when validators are updated
    }
    
    function $apply() { // the function that is called on the scope
      if (ngModel.$viewValue !== ngModel.$modelValue) {
        ngModel.$viewValue = ngModel.$modelValue;
        ngModel.$validate(); // this will trigger your validator
      } else {
        console.log('value not changed'); // the new value is no different than before, do not call $validate
      }
    }
    
    // your push is like doing :
    ngModel.$viewValue.push(12); 
    $apply(); // will output 'value not changed', because we changed the view value as well as the model value
    
    // whereas your should do:
    var newArray = [];
    // create a copy of the array (you can use angular.copy)
    for (var i = 0; i < myArray.length; i++) {
      newArray.push(myArray[i]);
    }
    ngModel.$viewValue.push(12);
    ngModel.$viewValue = newArray; // here we clearly update the $viewValue without changing the model value
    $apply(); // will output 'validators updated'
    

    Of course you are not forced to do an array copy. Instead, you can force the update of your ngModel. This is done by calling ngModel.$validate();

    One way of doing it would be to add a forceUpdate() function in your scope, and call it from the controller after you do a push();

    Example: http://jsfiddle.net/L7Lxkq1f/