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

fix(ngModel): don’t clear the model when an external validator failed #9016

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1646,14 +1646,6 @@ this limitation, use a regular expression object as the value for the expression
//after
$scope.exp = /abc/i;

- **NgModel:** due to [f3cb2741161353f387d02725637ce4ba062a9bc0](https://github.com/angular/angular.js/commit/f3cb2741161353f387d02725637ce4ba062a9bc0),

#### since 1.3.0-beta.11

If the user enters a value and a parser or validator fails, the model will be set to `undefined`.
This is the same behavior as in 1.2.x, but different to 1.3.0-beta.11, as there only invalid parsers
would set the model to `undefined`, but invalid validators would not change the model.

- **Scope:** due to [8c6a8171](https://github.com/angular/angular.js/commit/8c6a8171f9bdaa5cdabc0cc3f7d3ce10af7b434d),
Scope#$id is now of time number rather than string. Since the
id is primarily being used for debugging purposes this change should not affect
Expand Down
24 changes: 16 additions & 8 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1906,9 +1906,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$

// check parser error
if (!processParseErrors(parseValid)) {
validationDone(false);
return;
}
if (!processSyncValidators()) {
validationDone(false);
return;
}
processAsyncValidators();
Expand All @@ -1926,7 +1928,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
forEach(ctrl.$asyncValidators, function(v, name) {
setValidity(name, null);
});
validationDone();
return false;
}
}
Expand All @@ -1944,14 +1945,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
forEach(ctrl.$asyncValidators, function(v, name) {
setValidity(name, null);
});
validationDone();
return false;
}
return true;
}

function processAsyncValidators() {
var validatorPromises = [];
var allValid = true;
forEach(ctrl.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
Expand All @@ -1962,13 +1963,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
validatorPromises.push(promise.then(function() {
setValidity(name, true);
}, function(error) {
allValid = false;
setValidity(name, false);
}));
});
if (!validatorPromises.length) {
validationDone();
validationDone(true);
} else {
$q.all(validatorPromises).then(validationDone);
$q.all(validatorPromises).then(function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbosch It'd be nice to have a reject handler here as well in case the previous step rejects/throws. Right now it seems pretty safe, but future additions could introduce cases where an unhandled rejection is possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add noop explicitly, so that it is clear that we do want to do nothing when we have an error there. This makes sense as validationDone also calls the doneCallback conditionally...

validationDone(allValid);
});
}
}

Expand All @@ -1978,10 +1982,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
}
}

function validationDone() {
function validationDone(allValid) {
if (localValidationRunId === currentValidationRunId) {

doneCallback();
doneCallback(allValid);
}
}
};
Expand Down Expand Up @@ -2042,9 +2046,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$modelValue = modelValue;
writeToModelIfNeeded();
}
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
if (!allowInvalid) {
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
// Note: Don't check ctrl.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
ctrl.$modelValue = allValid ? modelValue : undefined;
writeToModelIfNeeded();
}
});
Expand Down
56 changes: 55 additions & 1 deletion test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@ describe('NgModelController', function() {
expect(ctrl.$modelValue).toBeUndefined();
});

it('should not reset the model when the view is invalid due to an external validator', function() {
ctrl.$setViewValue('aaaa');
expect(ctrl.$modelValue).toBe('aaaa');

ctrl.$setValidity('someExternalError', false);
ctrl.$setViewValue('bbbb');
expect(ctrl.$modelValue).toBe('bbbb');
});

it('should not reset the view when the view is invalid', function() {
// this test fails when the view changes the model and
// then the model listener in ngModel picks up the change and
Expand Down Expand Up @@ -302,6 +311,13 @@ describe('NgModelController', function() {
expect(ctrl.$error).toEqual({ high : true });
});

it('should not remove external validators when a parser failed', function() {
ctrl.$parsers.push(function(v) { return undefined; });
ctrl.$setValidity('externalError', false);
ctrl.$setViewValue('someValue');
expect(ctrl.$error).toEqual({ externalError : true, parse: true });
});

it('should remove all non-parse-related CSS classes from the form when a parser fails',
inject(function($compile, $rootScope) {

Expand Down Expand Up @@ -711,7 +727,7 @@ describe('NgModelController', function() {
expect(ctrl.$pending).toBeUndefined();
}));

it('should clear and ignore all pending promises when the input values changes', inject(function($q) {
it('should clear and ignore all pending promises when the model values changes', inject(function($q) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/changes/change

ctrl.$validators.sync = function(value) {
return true;
};
Expand Down Expand Up @@ -775,6 +791,44 @@ describe('NgModelController', function() {
expect(isObject(ctrl.$pending)).toBe(false);
}));

it('should clear all errors from async validators if a parser fails', inject(function($q) {
var failParser = false;
ctrl.$parsers.push(function(value) {
return failParser ? undefined : value;
});

ctrl.$asyncValidators.async = function(value) {
return $q.reject();
};

ctrl.$setViewValue('x..y..z');
expect(ctrl.$error).toEqual({async: true});

failParser = true;

ctrl.$setViewValue('1..2..3');
expect(ctrl.$error).toEqual({parse: true});
}));

it('should clear all errors from async validators if a sync validator fails', inject(function($q) {
var failValidator = false;
ctrl.$validators.sync = function(value) {
return !failValidator;
};

ctrl.$asyncValidators.async = function(value) {
return $q.reject();
};

ctrl.$setViewValue('x..y..z');
expect(ctrl.$error).toEqual({async: true});

failValidator = true;

ctrl.$setViewValue('1..2..3');
expect(ctrl.$error).toEqual({sync: true});
}));

it('should re-evaluate the form validity state once the asynchronous promise has been delivered',
inject(function($compile, $rootScope, $q) {

Expand Down