4

You can test it in this jsFiddle: HERE (better is to see on new jsFiddle, see EDIT part of this post)

I think there is a bug in AngularJS or at least not an expected result. If i detach a form then re-append it, it's class ng-invalid switch to ng-valid on re-append it to the DOM. This has for consequence to enable the submit button of the form even data aren't valid. Of course, i was expecting that validity status didn't switch.

I think it's a angular bug, but maybe a jquery one. I could use jquery to check on append if form was valid or not and then forcing form class but it's seems not working as a valid form get then the status of invalid. It's quite weird as i don't know any other workaround without using kind of data to saved status form before detaching it.

So anyone has already encountered this problem? Is there any method (if possible using AngularJS directive) to get rid of this bug?

PS: i need to detach form (and any other elements) in a single page web application to keep DOM as clean as possible.

EDIT

I've done a new jsFiddle that is illustrating more my problem, detaching content on internal site navigation: http://jsfiddle.net/EWVwa/

UPDATE

I come to this temporary solution (thanks to CaioToOn)

http://plnkr.co/edit/KIgMz2

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
});


app.directive('customValidation', function() {
  return {
    require: ['ngModel', '^?form'],
    link: function(scope, element, attr, ctrls) {
      console.log(ctrls);
      var ngModelCtrl = ctrls[0],
          formCtrl = ctrls[1];


      ngModelCtrl.$parsers.push(function(viewValue) {
        if (viewValue === 'test') {
          ngModelCtrl.$setValidity('name', true);
          formCtrl.$setValidity('name', true);
          return viewValue;
        } else {
          ngModelCtrl.$setValidity('name', false);
          formCtrl.$setValidity('name', false);
          return undefined;
        }
      });


      // custom event
      element.bind('$append', function() {
        formCtrl && formCtrl.$addControl(ngModelCtrl);
        /*** TEST for to keep form's validation status ***/
        formCtrl.$setValidity('name', ngModelCtrl.$valid);
        //ngModelCtrl.$setValidity('name', ngModelCtrl.$valid);
        console.log(formCtrl.$valid);
      });
      //binding on element, not scope. 
      element.bind('$destroy', function() {
        console.log("gone haven");        
      });
    }
  };
});

This need more testing regarding multiple inputs validation. I'll certainly updating answer when all tests will be done.

A. Wolff
  • 74,033
  • 9
  • 94
  • 155

1 Answers1

3

The problem happens because input directive removes itself from the form controls when the element is removed from DOM. As it doesn't link your ngModel and form controller again, your input is not being considered by the form anymore.

You basically have two three options:

  • change element visibility instead of removing it
  • (prefer the one below) exposing a "relink" function that would re-add it to the original form
  • triggering a custom event on all controls so they can relink theirselves

Changing element visibility means you will have unnecessary DOM elements in DOMTree. This is not quite bad, as you are keeping a reference to the $compile element anyway, so it will yet participate the $digest cycles and "DOM" modifications.

(After thinking for a while, the new solution is slightly better than this one, so don't expose a relinking function) Exposing a relink function is quite weird (although functional) and this is not the most reliable of the solutions. One way to achieve it consists in requiring form controller (require: ['ngModel', '^?form']) and binding a relinking function to the element's data:

element.data('relink', function(){
  formCtrl && formCtrl.$addControl(ngModelCtrl);
});

And when you add the element to the screen again, you gonna have to call all your controls relink function:

$('.controls').data('relink')();

See a example here.

It's not quite reliable, but might work for your case.

Triggering a custom event is pretty much the same as the previous, but you would dispatch a custom event on all elements that should relink theirselves. This is way more organized, but still not quite reliable, because the form and other links might also have been broken (again, should sufice your case). Basically listen to the custom event on your directive:

element.bind('$append', function(){
  formCtrl && formCtrl.$addControl(ngModelCtrl);
});

And after changing to the form, just trigger the custom event on all controls:

$('.control').triggerHandler('$append');

The reason why this one is better is that the directive still decides when to relink the component, and the event is kind of "generic". Here is a working plunker.

As a last effort, you could override jQuery.fn.append and trigger the custom event on all element children recursively (this is what Angular does when removing elements). This is the most organized, but it would impact yoour performance on ALL append calls.

Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • Thanks for this interesting post. I'll try it tomorow and let you know. "Exposing a relink function" method seems to be exactly what im looking for. – A. Wolff Apr 20 '13 at 19:01
  • Nice. I've change my "exposing a relink function" for a better solution (triggering a custom event on added elements). Will still have the same limitations and benefits as the previous, but is way better. – Caio Cunha Apr 21 '13 at 01:58
  • Using your Plunker, i was able to get exactly what i was looking for: http://plnkr.co/edit/KIgMz2 Form are now responding as expected after detach/append. I need to do more testing for multiple validation inputs in form (formCtrl.$setValidity('name', ngModelCtrl.$valid); should not be accurate in this case). BTW, i don't understand why 'required' key of 'Errors' obj is still set to false in my Plunker. I should use form.$error obj i think. That's said, thank you very much! – A. Wolff Apr 21 '13 at 13:30