Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 9314719

Browse files
committed
fix(ngModel): don’t clear the model when an external validator failed
Calling `ctrl.$setValidity()` with a an error key that does not belong to a validator in `ctrl.$validator` should not result in setting the model to `undefined` on the next input change. This bug was introduced in 1.3.0-beta.12. Closes #8357 Fixes #8080
1 parent 1418383 commit 9314719

File tree

3 files changed

+71
-17
lines changed

3 files changed

+71
-17
lines changed

CHANGELOG.md

-8
Original file line numberDiff line numberDiff line change
@@ -1871,14 +1871,6 @@ this limitation, use a regular expression object as the value for the expression
18711871
//after
18721872
$scope.exp = /abc/i;
18731873

1874-
- **NgModel:** due to [f3cb2741161353f387d02725637ce4ba062a9bc0](https://github.com/angular/angular.js/commit/f3cb2741161353f387d02725637ce4ba062a9bc0),
1875-
1876-
#### since 1.3.0-beta.11
1877-
1878-
If the user enters a value and a parser or validator fails, the model will be set to `undefined`.
1879-
This is the same behavior as in 1.2.x, but different to 1.3.0-beta.11, as there only invalid parsers
1880-
would set the model to `undefined`, but invalid validators would not change the model.
1881-
18821874
- **Scope:** due to [8c6a8171](https://github.com/angular/angular.js/commit/8c6a8171f9bdaa5cdabc0cc3f7d3ce10af7b434d),
18831875
Scope#$id is now of time number rather than string. Since the
18841876
id is primarily being used for debugging purposes this change should not affect

src/ng/directive/input.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -1906,9 +1906,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19061906

19071907
// check parser error
19081908
if (!processParseErrors(parseValid)) {
1909+
validationDone(false);
19091910
return;
19101911
}
19111912
if (!processSyncValidators()) {
1913+
validationDone(false);
19121914
return;
19131915
}
19141916
processAsyncValidators();
@@ -1926,7 +1928,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19261928
forEach(ctrl.$asyncValidators, function(v, name) {
19271929
setValidity(name, null);
19281930
});
1929-
validationDone();
19301931
return false;
19311932
}
19321933
}
@@ -1944,14 +1945,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19441945
forEach(ctrl.$asyncValidators, function(v, name) {
19451946
setValidity(name, null);
19461947
});
1947-
validationDone();
19481948
return false;
19491949
}
19501950
return true;
19511951
}
19521952

19531953
function processAsyncValidators() {
19541954
var validatorPromises = [];
1955+
var allValid = true;
19551956
forEach(ctrl.$asyncValidators, function(validator, name) {
19561957
var promise = validator(modelValue, viewValue);
19571958
if (!isPromiseLike(promise)) {
@@ -1962,13 +1963,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19621963
validatorPromises.push(promise.then(function() {
19631964
setValidity(name, true);
19641965
}, function(error) {
1966+
allValid = false;
19651967
setValidity(name, false);
19661968
}));
19671969
});
19681970
if (!validatorPromises.length) {
1969-
validationDone();
1971+
validationDone(true);
19701972
} else {
1971-
$q.all(validatorPromises).then(validationDone);
1973+
$q.all(validatorPromises).then(function() {
1974+
validationDone(allValid);
1975+
}, noop);
19721976
}
19731977
}
19741978

@@ -1978,10 +1982,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19781982
}
19791983
}
19801984

1981-
function validationDone() {
1985+
function validationDone(allValid) {
19821986
if (localValidationRunId === currentValidationRunId) {
19831987

1984-
doneCallback();
1988+
doneCallback(allValid);
19851989
}
19861990
}
19871991
};
@@ -2042,9 +2046,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20422046
ctrl.$modelValue = modelValue;
20432047
writeToModelIfNeeded();
20442048
}
2045-
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
2049+
ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
20462050
if (!allowInvalid) {
2047-
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
2051+
// Note: Don't check ctrl.$valid here, as we could have
2052+
// external validators (e.g. calculated on the server),
2053+
// that just call $setValidity and need the model value
2054+
// to calculate their validity.
2055+
ctrl.$modelValue = allValid ? modelValue : undefined;
20482056
writeToModelIfNeeded();
20492057
}
20502058
});

test/ng/directive/inputSpec.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ describe('NgModelController', function() {
214214
expect(ctrl.$modelValue).toBeUndefined();
215215
});
216216

217+
it('should not reset the model when the view is invalid due to an external validator', function() {
218+
ctrl.$setViewValue('aaaa');
219+
expect(ctrl.$modelValue).toBe('aaaa');
220+
221+
ctrl.$setValidity('someExternalError', false);
222+
ctrl.$setViewValue('bbbb');
223+
expect(ctrl.$modelValue).toBe('bbbb');
224+
});
225+
217226
it('should not reset the view when the view is invalid', function() {
218227
// this test fails when the view changes the model and
219228
// then the model listener in ngModel picks up the change and
@@ -302,6 +311,13 @@ describe('NgModelController', function() {
302311
expect(ctrl.$error).toEqual({ high : true });
303312
});
304313

314+
it('should not remove external validators when a parser failed', function() {
315+
ctrl.$parsers.push(function(v) { return undefined; });
316+
ctrl.$setValidity('externalError', false);
317+
ctrl.$setViewValue('someValue');
318+
expect(ctrl.$error).toEqual({ externalError : true, parse: true });
319+
});
320+
305321
it('should remove all non-parse-related CSS classes from the form when a parser fails',
306322
inject(function($compile, $rootScope) {
307323

@@ -711,7 +727,7 @@ describe('NgModelController', function() {
711727
expect(ctrl.$pending).toBeUndefined();
712728
}));
713729

714-
it('should clear and ignore all pending promises when the input values changes', inject(function($q) {
730+
it('should clear and ignore all pending promises when the model value changes', inject(function($q) {
715731
ctrl.$validators.sync = function(value) {
716732
return true;
717733
};
@@ -775,6 +791,44 @@ describe('NgModelController', function() {
775791
expect(isObject(ctrl.$pending)).toBe(false);
776792
}));
777793

794+
it('should clear all errors from async validators if a parser fails', inject(function($q) {
795+
var failParser = false;
796+
ctrl.$parsers.push(function(value) {
797+
return failParser ? undefined : value;
798+
});
799+
800+
ctrl.$asyncValidators.async = function(value) {
801+
return $q.reject();
802+
};
803+
804+
ctrl.$setViewValue('x..y..z');
805+
expect(ctrl.$error).toEqual({async: true});
806+
807+
failParser = true;
808+
809+
ctrl.$setViewValue('1..2..3');
810+
expect(ctrl.$error).toEqual({parse: true});
811+
}));
812+
813+
it('should clear all errors from async validators if a sync validator fails', inject(function($q) {
814+
var failValidator = false;
815+
ctrl.$validators.sync = function(value) {
816+
return !failValidator;
817+
};
818+
819+
ctrl.$asyncValidators.async = function(value) {
820+
return $q.reject();
821+
};
822+
823+
ctrl.$setViewValue('x..y..z');
824+
expect(ctrl.$error).toEqual({async: true});
825+
826+
failValidator = true;
827+
828+
ctrl.$setViewValue('1..2..3');
829+
expect(ctrl.$error).toEqual({sync: true});
830+
}));
831+
778832
it('should re-evaluate the form validity state once the asynchronous promise has been delivered',
779833
inject(function($compile, $rootScope, $q) {
780834

0 commit comments

Comments
 (0)