diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 6b93d55e67cb..81f98dcc8515 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1721,6 +1721,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
+ this.$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
this.$validators = {};
this.$asyncValidators = {};
this.$parsers = [];
@@ -1975,14 +1976,51 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @name ngModel.NgModelController#$validate
*
* @description
- * Runs each of the registered validators (first synchronous validators and then asynchronous validators).
+ * Runs each of the registered validators (first synchronous validators and then
+ * asynchronous validators).
+ * If the validity changes to invalid, the model will be set to `undefined`,
+ * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
+ * If the validity changes to valid, it will set the model to the last available valid
+ * modelValue, i.e. either the last parsed value or the last value set from the scope.
*/
this.$validate = function() {
// ignore $validate before model is initialized
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
return;
}
- this.$$parseAndValidate();
+
+ var viewValue = ctrl.$$lastCommittedViewValue;
+ // Note: we use the $$rawModelValue as $modelValue might have been
+ // set to undefined during a view -> model update that found validation
+ // errors. We can't parse the view here, since that could change
+ // the model although neither viewValue nor the model on the scope changed
+ 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 prevModelValue = ctrl.$modelValue;
+
+ var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+
+ ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
+ // If there was no change in validity, don't update the model
+ // This prevents changing an invalid modelValue to undefined
+ if (!allowInvalid && prevValid !== allValid) {
+ // 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;
+
+ if (ctrl.$modelValue !== prevModelValue) {
+ ctrl.$$writeModelToScope();
+ }
+ }
+ });
+
};
this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) {
@@ -2130,6 +2168,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+ ctrl.$$rawModelValue = modelValue;
if (allowInvalid) {
ctrl.$modelValue = modelValue;
writeToModelIfNeeded();
@@ -2254,7 +2293,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// if scope model value and ngModel value are out of sync
// TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue) {
- ctrl.$modelValue = modelValue;
+ ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
var formatters = ctrl.$formatters,
idx = formatters.length;
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index 8b1fbe885608..b157ab9bbf58 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -505,6 +505,121 @@ describe('NgModelController', function() {
expect(ctrl.$valid).toBe(true);
});
+ it('should pass the last parsed modelValue to the validators', function() {
+ ctrl.$parsers.push(function(modelValue) {
+ return modelValue + 'def';
+ });
+
+ ctrl.$setViewValue('abc');
+
+ ctrl.$validators.test = function(modelValue, viewValue) {
+ return true;
+ };
+
+ spyOn(ctrl.$validators, 'test');
+
+ ctrl.$validate();
+
+ expect(ctrl.$validators.test).toHaveBeenCalledWith('abcdef', 'abc');
+ });
+
+ it('should set the model to undefined when it becomes invalid', function() {
+ var valid = true;
+ ctrl.$validators.test = function(modelValue, viewValue) {
+ return valid;
+ };
+
+ scope.$apply('value = "abc"');
+ expect(scope.value).toBe('abc');
+
+ valid = false;
+ ctrl.$validate();
+
+ expect(scope.value).toBeUndefined();
+ });
+
+ it('should update the model when it becomes valid', function() {
+ var valid = true;
+ ctrl.$validators.test = function(modelValue, viewValue) {
+ return valid;
+ };
+
+ scope.$apply('value = "abc"');
+ expect(scope.value).toBe('abc');
+
+ valid = false;
+ ctrl.$validate();
+ expect(scope.value).toBeUndefined();
+
+ valid = true;
+ ctrl.$validate();
+ expect(scope.value).toBe('abc');
+ });
+
+ it('should not update the model when it is valid, but there is a parse error', function() {
+ ctrl.$parsers.push(function(modelValue) {
+ return undefined;
+ });
+
+ ctrl.$setViewValue('abc');
+ expect(ctrl.$error.parse).toBe(true);
+ expect(scope.value).toBeUndefined();
+
+ ctrl.$validators.test = function(modelValue, viewValue) {
+ return true;
+ };
+
+ ctrl.$validate();
+ expect(ctrl.$error).toEqual({parse: true});
+ expect(scope.value).toBeUndefined();
+ });
+
+ it('should not set an invalid model to undefined when validity is the same', function() {
+ ctrl.$validators.test = function() {
+ return false;
+ };
+
+ scope.$apply('value = "invalid"');
+ expect(ctrl.$valid).toBe(false);
+ expect(scope.value).toBe('invalid');
+
+ ctrl.$validate();
+ expect(ctrl.$valid).toBe(false);
+ expect(scope.value).toBe('invalid');
+ });
+
+ it('should not change a model that has a formatter', function() {
+ ctrl.$validators.test = function() {
+ return true;
+ };
+
+ ctrl.$formatters.push(function(modelValue) {
+ return 'xyz';
+ });
+
+ scope.$apply('value = "abc"');
+ expect(ctrl.$viewValue).toBe('xyz');
+
+ ctrl.$validate();
+ expect(scope.value).toBe('abc');
+ });
+
+ it('should not change a model that has a parser', function() {
+ ctrl.$validators.test = function() {
+ return true;
+ };
+
+ ctrl.$parsers.push(function(modelValue) {
+ return 'xyz';
+ });
+
+ scope.$apply('value = "abc"');
+
+ ctrl.$validate();
+ expect(scope.value).toBe('abc');
+ });
+ });
+
describe('view -> model update', function() {
it('should always perform validations using the parsed model value', function() {
var captures;
@@ -937,6 +1052,7 @@ describe('NgModelController', function() {
});
});
+
describe('ngModel', function() {
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -1351,7 +1467,6 @@ describe('input', function() {
expect(scope.form.$$renameControl).not.toHaveBeenCalled();
});
-
describe('compositionevents', function() {
it('should not update the model between "compositionstart" and "compositionend" on non android', inject(function($sniffer) {
$sniffer.android = false;
@@ -2314,6 +2429,14 @@ describe('input', function() {
expect(inputElm).toBeValid();
expect(scope.form.input.$error.minlength).not.toBe(true);
});
+
+ it('should validate when the model is initalized as a number', function() {
+ scope.value = 12345;
+ compileInput('');
+ expect(scope.value).toBe(12345);
+ expect(scope.form.input.$error.minlength).toBeUndefined();
+ });
+
});
@@ -2412,6 +2535,13 @@ describe('input', function() {
expect(scope.value).toBe('12345');
});
+ it('should validate when the model is initalized as a number', function() {
+ scope.value = 12345;
+ compileInput('');
+ expect(scope.value).toBe(12345);
+ expect(scope.form.input.$error.maxlength).toBeUndefined();
+ });
+
});