diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index fa6fe55d9f5e..b7823961491b 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1757,7 +1757,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * Runs each of the registered validations set on the $validators object. */ this.$validate = function() { - this.$$runValidators(ctrl.$modelValue, ctrl.$viewValue); + this.$$runValidators(ctrl.$$validateValue, ctrl.$viewValue); }; this.$$runValidators = function(modelValue, viewValue) { @@ -1766,26 +1766,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }); }; - /** - * @ngdoc method - * @name ngModel.NgModelController#$commitViewValue - * - * @description - * Commit a pending update to the `$modelValue`. - * - * Updates may be pending by a debounced event or because the input is waiting for a some future - * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` - * usually handles calling this in response to input events. - */ - this.$commitViewValue = function() { + this.$$validateViewValue = function() { var viewValue = ctrl.$viewValue; - $timeout.cancel(pendingDebounce); - if (ctrl.$$lastCommittedViewValue === viewValue) { - return; - } - ctrl.$$lastCommittedViewValue = viewValue; - // change to dirty if (ctrl.$pristine) { ctrl.$dirty = true; @@ -1804,9 +1787,38 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ (isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) { ctrl.$$runValidators(modelValue, viewValue); - ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + ctrl.$$validateValue = ctrl.$valid ? modelValue : undefined; ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue; + return ctrl.$valid ? modelValue : undefined; + } + + return ctrl.$modelValue; + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$commitViewValue + * + * @description + * Commit a pending update to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(pendingDebounce); + if (ctrl.$$lastCommittedViewValue === viewValue) { + return; + } + ctrl.$$lastCommittedViewValue = viewValue; + + var modelValue = ctrl.$$validateViewValue(); + if (ctrl.$modelValue !== modelValue) { + ctrl.$modelValue = modelValue; ngModelSet($scope, ctrl.$modelValue); forEach(ctrl.$viewChangeListeners, function(listener) { try { @@ -1848,6 +1860,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ */ this.$setViewValue = function(value, trigger) { ctrl.$viewValue = value; + if (ctrl.$options && ctrl.$options.validateOnDefault) { + ctrl.$$validateViewValue(); + } if (!ctrl.$options || ctrl.$options.updateOnDefault) { ctrl.$$debounceViewValueCommit(trigger); } @@ -1897,6 +1912,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$$runValidators(modelValue, viewValue); ctrl.$modelValue = ctrl.$valid ? modelValue : undefined; + ctrl.$$validateValue = ctrl.$modelValue; ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue; if (ctrl.$viewValue !== viewValue) { @@ -2048,6 +2064,13 @@ var ngModelDirective = function() { }); }); } + if (modelCtrl.$options && modelCtrl.$options.validateOn) { + element.on(modelCtrl.$options.validateOn, function(ev) { + scope.$apply(function() { + modelCtrl.$$validateViewValue(); + }); + }); + } element.on('blur', function(ev) { scope.$apply(function() { @@ -2504,6 +2527,13 @@ var ngModelOptionsDirective = function() { that.$options.updateOnDefault = true; return ' '; })); + + if (this.$options.validateOn !== undefined) { + this.$options.validateOn = trim(this.$options.validateOn.replace(DEFAULT_REGEXP, function() { + that.$options.validateOnDefault = true; + return ' '; + })); + } } else { this.$options.updateOnDefault = true; } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index e48a2a082672..310d83b8f3b5 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -293,12 +293,12 @@ describe('NgModelController', function() { return (/^[A-Z]+$/).test(value); }; - ctrl.$modelValue = 'test'; + ctrl.$$validateValue = 'test'; ctrl.$validate(); expect(ctrl.$valid).toBe(false); - ctrl.$modelValue = 'TEST'; + ctrl.$$validateValue = 'TEST'; ctrl.$validate(); expect(ctrl.$valid).toBe(true); @@ -309,12 +309,12 @@ describe('NgModelController', function() { return (/^[A-Z]+$/).test(value); }; - ctrl.$modelValue = 'test'; + ctrl.$$validateValue = 'test'; ctrl.$validate(); expect(ctrl.$valid).toBe(false); - ctrl.$modelValue = 'TEST'; + ctrl.$$validateValue = 'TEST'; ctrl.$validate(); expect(ctrl.$valid).toBe(true); @@ -846,6 +846,36 @@ describe('input', function() { expect(scope.name).toEqual('a'); }); + it('should allow validating before view value is committed', function() { + scope.value = 2; + compileInput( + ''); + changeInputValueTo('20'); + expect(scope.form.alias.$error.max).toBeTruthy(); + expect(formElm).toBeDirty(); + expect(scope.value).toEqual(2); + browserTrigger(inputElm, 'blur'); + expect(scope.value).toBeUndefined(); + }); + + it('should allow defining trigger for validation', function() { + scope.value = 2; + compileInput( + ''); + changeInputValueTo('20'); + expect(scope.form.alias.$error.max).toBeFalsy(); + browserTrigger(inputElm, 'blur'); + expect(scope.form.alias.$error.max).toBeTruthy(); + expect(formElm).toBeDirty(); + expect(scope.value).toEqual(2); + browserTrigger(formElm, 'submit'); + expect(scope.value).toBeUndefined(); + }); + it('should not dirty the input if nothing was changed before updateOn trigger', function() { compileInput( '