aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIgor Minar2013-11-04 10:32:23 -0800
committerIgor Minar2013-11-04 10:35:51 -0800
commit8f989d652f70fd147f66a18411070c7b939e242e (patch)
tree5400ef23b01d7b3f90428fd595fc8ac3af0c8162
parent9483373c331343648e079420b3eb1f564d410ff2 (diff)
downloadangular.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.js2
-rw-r--r--test/ng/directive/formSpec.js21
-rw-r--r--test/ng/directive/inputSpec.js89
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()" />');