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

Commit

Permalink
fix(ngModel): fix issues when parserName is same as validator key
Browse files Browse the repository at this point in the history
For $validate(), it is necessary to store the parseError state
in the controller. Otherwise, if the parser name equals a validator
key, $validate() will assume a parse error occured if the validator
is invalid.

Also, setting the validity for the parser now happens after setting
validity for the validator key. Otherwise, the parse key is set,
and then immediately afterwards the validator key is unset
(because parse errors remove all other validations).

Fixes #10698
Closes #10850
Closes #11046
  • Loading branch information
petebacondarwin committed Feb 13, 2015
1 parent 27fcca9 commit 056a317
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 15 deletions.
29 changes: 14 additions & 15 deletions src/ng/directive/ngModel.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ngModelGet = parsedNgModel, ngModelGet = parsedNgModel,
ngModelSet = parsedNgModelAssign, ngModelSet = parsedNgModelAssign,
pendingDebounce = null, pendingDebounce = null,
parserValid,
ctrl = this; ctrl = this;


this.$$setOptions = function(options) { this.$$setOptions = function(options) {
Expand Down Expand Up @@ -516,16 +517,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// the model although neither viewValue nor the model on the scope changed // the model although neither viewValue nor the model on the scope changed
var modelValue = ctrl.$$rawModelValue; var modelValue = ctrl.$$rawModelValue;


// Check if the there's a parse error, so we don't unset it accidentially
var parserName = ctrl.$$parserName || 'parse';
var parserValid = ctrl.$error[parserName] ? false : undefined;

var prevValid = ctrl.$valid; var prevValid = ctrl.$valid;
var prevModelValue = ctrl.$modelValue; var prevModelValue = ctrl.$modelValue;


var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;


ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
// If there was no change in validity, don't update the model // If there was no change in validity, don't update the model
// This prevents changing an invalid modelValue to undefined // This prevents changing an invalid modelValue to undefined
if (!allowInvalid && prevValid !== allValid) { if (!allowInvalid && prevValid !== allValid) {
Expand All @@ -543,12 +540,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$


}; };


this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { this.$$runValidators = function(modelValue, viewValue, doneCallback) {
currentValidationRunId++; currentValidationRunId++;
var localValidationRunId = currentValidationRunId; var localValidationRunId = currentValidationRunId;


// check parser error // check parser error
if (!processParseErrors(parseValid)) { if (!processParseErrors()) {
validationDone(false); validationDone(false);
return; return;
} }
Expand All @@ -558,21 +555,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
} }
processAsyncValidators(); processAsyncValidators();


function processParseErrors(parseValid) { function processParseErrors() {
var errorKey = ctrl.$$parserName || 'parse'; var errorKey = ctrl.$$parserName || 'parse';
if (parseValid === undefined) { if (parserValid === undefined) {
setValidity(errorKey, null); setValidity(errorKey, null);
} else { } else {
setValidity(errorKey, parseValid); if (!parserValid) {
if (!parseValid) {
forEach(ctrl.$validators, function(v, name) { forEach(ctrl.$validators, function(v, name) {
setValidity(name, null); setValidity(name, null);
}); });
forEach(ctrl.$asyncValidators, function(v, name) { forEach(ctrl.$asyncValidators, function(v, name) {
setValidity(name, null); setValidity(name, null);
}); });
return false;
} }
// Set the parse error last, to prevent unsetting it, should a $validators key == parserName
setValidity(errorKey, parserValid);
return parserValid;
} }
return true; return true;
} }
Expand Down Expand Up @@ -667,7 +665,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$$parseAndValidate = function() { this.$$parseAndValidate = function() {
var viewValue = ctrl.$$lastCommittedViewValue; var viewValue = ctrl.$$lastCommittedViewValue;
var modelValue = viewValue; var modelValue = viewValue;
var parserValid = isUndefined(modelValue) ? undefined : true; parserValid = isUndefined(modelValue) ? undefined : true;


if (parserValid) { if (parserValid) {
for (var i = 0; i < ctrl.$parsers.length; i++) { for (var i = 0; i < ctrl.$parsers.length; i++) {
Expand All @@ -693,7 +691,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$


// Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
// This can happen if e.g. $setViewValue is called from inside a parser // This can happen if e.g. $setViewValue is called from inside a parser
ctrl.$$runValidators(parserValid, modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
if (!allowInvalid) { if (!allowInvalid) {
// Note: Don't check ctrl.$valid here, as we could have // Note: Don't check ctrl.$valid here, as we could have
// external validators (e.g. calculated on the server), // external validators (e.g. calculated on the server),
Expand Down Expand Up @@ -814,6 +812,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// TODO(perf): why not move this to the action fn? // TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue) { if (modelValue !== ctrl.$modelValue) {
ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
parserValid = undefined;


var formatters = ctrl.$formatters, var formatters = ctrl.$formatters,
idx = formatters.length; idx = formatters.length;
Expand All @@ -826,7 +825,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render(); ctrl.$render();


ctrl.$$runValidators(undefined, modelValue, viewValue, noop); ctrl.$$runValidators(modelValue, viewValue, noop);
} }
} }


Expand Down
90 changes: 90 additions & 0 deletions test/ng/directive/ngModelSpec.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1221,6 +1221,96 @@ describe('ngModel', function() {
expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'ab'); expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'ab');
expect(ctrl.$validators.mock.calls.length).toEqual(2); expect(ctrl.$validators.mock.calls.length).toEqual(2);
}); });

it('should validate correctly when $parser name equals $validator key', function() {

ctrl.$validators.parserOrValidator = function(value) {
switch (value) {
case 'allInvalid':
case 'parseValid-validatorsInvalid':
case 'stillParseValid-validatorsInvalid':
return false;
default:
return true;
}
};

ctrl.$validators.validator = function(value) {
switch (value) {
case 'allInvalid':
case 'parseValid-validatorsInvalid':
case 'stillParseValid-validatorsInvalid':
return false;
default:
return true;
}
};

ctrl.$$parserName = 'parserOrValidator';
ctrl.$parsers.push(function(value) {
switch (value) {
case 'allInvalid':
case 'stillAllInvalid':
case 'parseInvalid-validatorsValid':
case 'stillParseInvalid-validatorsValid':
return undefined;
default:
return value;
}
});

//Parser and validators are invalid
scope.$apply('value = "allInvalid"');
expect(scope.value).toBe('allInvalid');
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

ctrl.$validate();
expect(scope.value).toEqual('allInvalid');
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

ctrl.$setViewValue('stillAllInvalid');
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true});

ctrl.$validate();
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true});

//Parser is valid, validators are invalid
scope.$apply('value = "parseValid-validatorsInvalid"');
expect(scope.value).toBe('parseValid-validatorsInvalid');
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

ctrl.$validate();
expect(scope.value).toBe('parseValid-validatorsInvalid');
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

ctrl.$setViewValue('stillParseValid-validatorsInvalid');
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

ctrl.$validate();
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});

//Parser is invalid, validators are valid
scope.$apply('value = "parseInvalid-validatorsValid"');
expect(scope.value).toBe('parseInvalid-validatorsValid');
expect(ctrl.$error).toEqual({});

ctrl.$validate();
expect(scope.value).toBe('parseInvalid-validatorsValid');
expect(ctrl.$error).toEqual({});

ctrl.$setViewValue('stillParseInvalid-validatorsValid');
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true});

ctrl.$validate();
expect(scope.value).toBeUndefined();
expect(ctrl.$error).toEqual({parserOrValidator: true});
});

}); });
}); });


Expand Down

0 comments on commit 056a317

Please sign in to comment.