Abraham P
Abraham P

Reputation: 15481

An invalidated form does not get its validity recalculated when offending element removed from DOM

I use ctrl.$setValidity within one of my directives to invalidate a form. However, there is a method residing elsewhere which could remove this element from the DOM under a different set of conditions. What happens when I invalidate the form with $setValidity and then remove the offending element is that the form remains invalid, while what I would like it to do is to recalculate its validity based on its new Inputfield set.

Please note that I am not simply looking for a ctrl.$setValidity true, (other input fields in the form may or may not be valid), I simply want the form to recalculate.

Below is the directive code:

 app.directive('dir', function() {
   return {
      restrict: 'C',
      require: "ngModel",
      link: function(scope, element, attrs, ctrl) {
      someValue = scope.someValue;
      someField = element.find(".some-class");

      monitorField = function(newValue, oldValue){
        console.log("whee");
        scope.someFunction(newValue);
        if(newValue =="Invalidate NOW!"){
        ctrl.$setValidity('someClass', false);      
        } else if (oldValue == "Invalidate NOW!"){
           angular.element(document.querySelector('.some-class')).remove();
        } else {
          ctrl.$setValidity('someClass', true); 
       }
    }  
    scope.$watch("someValue", monitorField, true);
   }
 }
});

Which operates on:

   <FORM class="dir" ng-model="someValue">
     <INPUT type="text" class="some-class" ng-model="someValue"/>
     <INPUT type="text" class="some-other-class"/>
   </FORM>

(Yes it is a somewhat contrived example).

The problem is reproduced here: http://jsfiddle.net/kTuAY/

In my actual code, I populate the input fields based on an array of objects, which gets populated via Service, and remove elements via array.splice. The example given in the jsfiddle is merely there for simplicity.

Another interesting condition of failure can be seen in this fiddle:

http://jsfiddle.net/JACAv/

Specifically, if one of the inputs depends on non collision of value with the other, and is thusly invalidated, whereafter the other fields value is changed, validity remains incorrect.

Temporary not quite functioning fiddle while I go to work:

http://jsfiddle.net/yQpHL/

Thanks!

Upvotes: 2

Views: 504

Answers (3)

Abraham P
Abraham P

Reputation: 15481

The way I ended up solving this was:

  1. Put a separate watch expression on the array being ng-repeated over.
  2. Within that watch expression, construct a hash of all the values currently present in input fields that match the relevant directive.
  3. Reset the value of those fields forcefully with .val(), and then call $compile on the inner fields to retrigger the inner watch expression
  4. Within the function adding the input field values on the controller (the function that gets called just before ctrl.$setValidity from the inner directive:
    • Verify that he number of times the value is entered is 1
    • Reset the array being ng-repeated over to contain the values of the hash

The good:

  • When an element is removed,the outer watch (the one on the entire array) triggers, the hash is recomputed, the values of each field are reset and revalidated
  • When a collision is detected, it will bubble correctly, marking all colliding fields as invalid, when the collision is removed, all fields are re-validated
The bad:
  • Calling $compile on every field every time the array changes is computationally inefficient
  • As the inner watch expression will trigger once when the value changes, and once when the outer watch expression calls compile, effectively the number of $digest's is doubled

I'll put together a code example just as soon as I sanitize the code.

Upvotes: 0

Ben Lesh
Ben Lesh

Reputation: 108501

Removal of a DOM element that is linked to part of your model should be done by removing the part of the $scoped object the model is pointing at if possible. Think of the basic ngRepeat, if you remove an item from the array, it removes an element from the DOM. Your form should work the same way. This will prevent that from happening. All of the validation information is contained on the model in your $scope... like so: $scope.myForm.model.$error.required for example. If you remove the $scope property or array item that the model is pointing at (e.g. $scope.foo or $scope.items.splice(2,1), Angular will know (presumably during the next $digest) to get rid of the model as well as it's validation information.

It doesn't matter if you remove the item in a controller or in a directive, but it should be removed from the $scope, then a digest needs to occur for the UI and validation to update.

I hope that helps, because I feel like I stumbled over what I was trying to say...

Upvotes: 2

mfeingold
mfeingold

Reputation: 7134

I do not think the problem is in the directive, it is in the 'other' code

If the DOM element removal is done n response to 'native' events - i.e. drag'n drop events, ajax events or something, the angular framework would not know to run the recalculation by itself. In this code after you are done updating you have to call $scope.$apply() on a proper scope.

Oh I see your fiddle... ok. How about making life a little easier - instead of removing your DOM element why do not you decorate it with a ng-show attribute and let angular take care of the rest?

Upvotes: 0

Related Questions