diff options
| author | Igor Minar | 2013-11-04 10:32:23 -0800 |
|---|---|---|
| committer | Igor Minar | 2013-11-04 10:35:51 -0800 |
| commit | 8f989d652f70fd147f66a18411070c7b939e242e (patch) | |
| tree | 5400ef23b01d7b3f90428fd595fc8ac3af0c8162 | |
| parent | 9483373c331343648e079420b3eb1f564d410ff2 (diff) | |
| download | angular.js-8f989d652f70fd147f66a18411070c7b939e242e.tar.bz2 | |
fix(ngModel): deregister from the form on scope not DOM destruction
Due to animations, DOM might get destroyed much later than scope and so the element $destroy event
might get fired outside of $digest, which causes changes to the validation model go unobserved
until the next digest. By deregistering on scope event, the deregistration always happens
in $digest and the form validation model changes will be observed.
Closes #4226
Closes #4779
| -rw-r--r-- | src/ng/directive/input.js | 2 | ||||
| -rw-r--r-- | test/ng/directive/formSpec.js | 21 | ||||
| -rw-r--r-- | test/ng/directive/inputSpec.js | 89 |
3 files changed, 94 insertions, 18 deletions
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index cc97411a..1c37167d 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1222,7 +1222,7 @@ var ngModelDirective = function() { formCtrl.$addControl(modelCtrl); - element.on('$destroy', function() { + scope.$on('$destroy', function() { formCtrl.$removeControl(modelCtrl); }); } diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js index 53fd3d90..77beb2fd 100644 --- a/test/ng/directive/formSpec.js +++ b/test/ng/directive/formSpec.js @@ -36,18 +36,23 @@ describe('form', function() { }); - it('should remove the widget when element removed', function() { + it('should remove form control references from the form when nested control is removed from the DOM', function() { doc = $compile( '<form name="myForm">' + - '<input type="text" name="alias" ng-model="value" store-model-ctrl/>' + + '<input ng-if="inputPresent" name="alias" ng-model="value" store-model-ctrl/>' + '</form>')(scope); + scope.inputPresent = true; + scope.$digest(); var form = scope.myForm; control.$setValidity('required', false); expect(form.alias).toBe(control); expect(form.$error.required).toEqual([control]); - doc.find('input').remove(); + // remove nested control + scope.inputPresent = false; + scope.$apply(); + expect(form.$error.required).toBe(false); expect(form.alias).toBeUndefined(); }); @@ -362,14 +367,15 @@ describe('form', function() { }); - it('should deregister a input when its removed from DOM', function() { + it('should deregister a input when it is removed from DOM', function() { doc = jqLite( '<form name="parent">' + '<div class="ng-form" name="child">' + - '<input ng:model="modelA" name="inputA" required>' + + '<input ng-if="inputPresent" ng-model="modelA" name="inputA" required>' + '</div>' + '</form>'); $compile(doc)(scope); + scope.inputPresent = true; scope.$apply(); var parent = scope.parent, @@ -384,7 +390,10 @@ describe('form', function() { expect(doc.hasClass('ng-invalid-required')).toBe(true); expect(doc.find('div').hasClass('ng-invalid')).toBe(true); expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true); - doc.find('input').remove(); //remove child + + //remove child input + scope.inputPresent = false; + scope.$apply(); expect(parent.$error.required).toBe(false); expect(child.$error.required).toBe(false); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index c60960f0..3783c9ed 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -305,6 +305,84 @@ describe('ngModel', function() { expect(element).toBeInvalid(); expect(element).toHaveClass('ng-invalid-required'); })); + + + it('should register/deregister a nested ngModel with parent form when entering or leaving DOM', + inject(function($compile, $rootScope) { + + var element = $compile('<form name="myForm">' + + '<input ng-if="inputPresent" name="myControl" ng-model="value" required >' + + '</form>')($rootScope); + var isFormValid; + + $rootScope.inputPresent = false; + $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); + + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(true); + expect(isFormValid).toBe(true); + expect($rootScope.myForm.myControl).toBeUndefined(); + + $rootScope.inputPresent = true; + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(false); + expect(isFormValid).toBe(false); + expect($rootScope.myForm.myControl).toBeDefined(); + + $rootScope.inputPresent = false; + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(true); + expect(isFormValid).toBe(true); + expect($rootScope.myForm.myControl).toBeUndefined(); + + dealoc(element); + })); + + + it('should register/deregister a nested ngModel with parent form when entering or leaving DOM with animations', + function() { + + // ngAnimate performs the dom manipulation after digest, and since the form validity can be affected by a form + // control going away we must ensure that the deregistration happens during the digest while we are still doing + // dirty checking. + module('ngAnimate'); + + inject(function($compile, $rootScope) { + var element = $compile('<form name="myForm">' + + '<input ng-if="inputPresent" name="myControl" ng-model="value" required >' + + '</form>')($rootScope); + var isFormValid; + + $rootScope.inputPresent = false; + // this watch ensure that the form validity gets updated during digest (so that we can observe it) + $rootScope.$watch('myForm.$valid', function(value) { isFormValid = value; }); + + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(true); + expect(isFormValid).toBe(true); + expect($rootScope.myForm.myControl).toBeUndefined(); + + $rootScope.inputPresent = true; + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(false); + expect(isFormValid).toBe(false); + expect($rootScope.myForm.myControl).toBeDefined(); + + $rootScope.inputPresent = false; + $rootScope.$apply(); + + expect($rootScope.myForm.$valid).toBe(true); + expect(isFormValid).toBe(true); + expect($rootScope.myForm.myControl).toBeUndefined(); + + dealoc(element); + }); + }); }); @@ -369,17 +447,6 @@ describe('input', function() { }); - it('should cleanup it self from the parent form', function() { - compileInput('<input ng-model="name" name="alias" required>'); - - scope.$apply(); - expect(scope.form.$error.required.length).toBe(1); - - inputElm.remove(); - expect(scope.form.$error.required).toBe(false); - }); - - it('should update the model on "blur" event', function() { compileInput('<input type="text" ng-model="name" name="alias" ng-change="change()" />'); |
