diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 527e1c355c92..6a700ec552e0 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -2037,12 +2037,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$modelValue = ngModelGet(); } var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + if (allowInvalid) { + ctrl.$modelValue = modelValue; + writeToModelIfNeeded(); + } ctrl.$$runValidators(parserValid, modelValue, viewValue, function() { - ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + if (!allowInvalid) { + ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { if (ctrl.$modelValue !== prevModelValue) { ctrl.$$writeModelToScope(); } - }); + } }; this.$$writeModelToScope = function() { @@ -2764,6 +2775,8 @@ var ngValueDirective = function() { * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` + * - `allowInvalid`: boolean value which indicates that the model can be set with values that did + * not validate correctly instead of the default behavior of setting the model to undefined. * - `getterSetter`: boolean value which determines whether or not to treat functions bound to `ngModel` as getters/setters. * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 33dcbd688c0c..169c8d6da9b7 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1769,6 +1769,79 @@ describe('input', function() { 'ng-model-options="{ getterSetter: true }" />'); }); + it('should assign invalid values to the scope if allowInvalid is true', function() { + compileInput(''); + changeInputValueTo('12345'); + + expect(scope.value).toBe('12345'); + expect(inputElm).toBeInvalid(); + }); + + it('should not assign not parsable values to the scope if allowInvalid is true', function() { + compileInput('', { + valid: false, + badInput: true + }); + changeInputValueTo('abcd'); + + expect(scope.value).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + it('should update the scope before async validators execute if allowInvalid is true', inject(function($q) { + compileInput(''); + var defer; + scope.form.input.$asyncValidators.promiseValidator = function(value) { + defer = $q.defer(); + return defer.promise; + }; + changeInputValueTo('12345'); + + expect(scope.value).toBe('12345'); + expect(scope.form.input.$pending.promiseValidator).toBe(true); + defer.reject(); + scope.$digest(); + expect(scope.value).toBe('12345'); + expect(inputElm).toBeInvalid(); + })); + + it('should update the view before async validators execute if allowInvalid is true', inject(function($q) { + compileInput(''); + var defer; + scope.form.input.$asyncValidators.promiseValidator = function(value) { + defer = $q.defer(); + return defer.promise; + }; + scope.$apply('value = \'12345\''); + + expect(inputElm.val()).toBe('12345'); + expect(scope.form.input.$pending.promiseValidator).toBe(true); + defer.reject(); + scope.$digest(); + expect(inputElm.val()).toBe('12345'); + expect(inputElm).toBeInvalid(); + })); + + it('should not call ng-change listeners twice if the model did not change with allowInvalid', function() { + compileInput(''); + scope.changed = jasmine.createSpy('changed'); + scope.form.input.$parsers.push(function(value) { + return 'modelValue'; + }); + + changeInputValueTo('input1'); + expect(scope.value).toBe('modelValue'); + expect(scope.changed).toHaveBeenCalledOnce(); + + changeInputValueTo('input2'); + expect(scope.value).toBe('modelValue'); + expect(scope.changed).toHaveBeenCalledOnce(); + }); }); it('should allow complex reference binding', function() {