diff --git a/src/.jshintrc b/src/.jshintrc index fd0170bc6661..d4912eafcf34 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -100,6 +100,7 @@ "assertNotHasOwnProperty": false, "getter": false, "getBlockElements": false, + "VALIDITY_STATE_PROPERTY": false, /* AngularPublic.js */ "version": false, diff --git a/src/Angular.js b/src/Angular.js index e09351d9b583..5916174504b3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -13,6 +13,7 @@ -angularModule, -nodeName_, -uid, + -VALIDITY_STATE_PROPERTY, -lowercase, -uppercase, @@ -102,6 +103,10 @@ * <div doc-module-components="ng"></div> */ +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + /** * @ngdoc function * @name angular.lowercase diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index c1eaac7062b1..951a4dfa8865 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -435,15 +435,29 @@ function validate(ctrl, validatorName, validity, value){ return validity ? value : undefined; } +function testFlags(validity, flags) { + var i, flag; + if (flags) { + for (i=0; i<flags.length; ++i) { + flag = flags[i]; + if (validity[flag]) { + return true; + } + } + } + return false; +} -function addNativeHtml5Validators(ctrl, validatorName, element) { - var validity = element.prop('validity'); +// Pass validity so that behaviour can be mocked easier. +function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) { if (isObject(validity)) { + ctrl.$$hasNativeValidators = true; var validator = function(value) { // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can // perform the required validation) - if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || - validity.typeMismatch) && !validity.valueMissing) { + if (!ctrl.$error[validatorName] && + !testFlags(validity, ignoreFlags) && + testFlags(validity, badFlags)) { ctrl.$setValidity(validatorName, false); return; } @@ -454,8 +468,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) { } function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var validity = element.prop('validity'); + var validity = element.prop(VALIDITY_STATE_PROPERTY); var placeholder = element[0].placeholder, noevent = {}; + ctrl.$$validityState = validity; // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. @@ -493,11 +508,11 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { value = trim(value); } - if (ctrl.$viewValue !== value || - // If the value is still empty/falsy, and there is no `required` error, run validators - // again. This enables HTML5 constraint validation errors to affect Angular validation - // even when the first character entered causes an error. - (validity && value === '' && !validity.valueMissing)) { + // If a control is suffering from bad input, browsers discard its value, so it may be + // necessary to revalidate even if the control's value is the same empty value twice in + // a row. + var revalidate = validity && ctrl.$$hasNativeValidators; + if (ctrl.$viewValue !== value || (value === '' && revalidate)) { if (scope.$$phase) { ctrl.$setViewValue(value); } else { @@ -603,6 +618,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } } +var numberBadFlags = ['badInput']; + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { textInputType(scope, element, attr, ctrl, $sniffer, $browser); @@ -617,7 +634,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }); - addNativeHtml5Validators(ctrl, 'number', element); + addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState); ctrl.$formatters.push(function(value) { return ctrl.$isEmpty(value) ? '' : '' + value; diff --git a/test/.jshintrc b/test/.jshintrc index f369426120d1..d4c3479482ff 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -101,6 +101,7 @@ "assertNotHasOwnProperty": false, "getter": false, "getBlockElements": false, + "VALIDITY_STATE_PROPERTY": true, /* filters.js */ "getFirstThursdayOfYear": false, diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index d14e1a2601c0..b5c6918f23bc 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -410,15 +410,33 @@ describe('ngModel', function() { describe('input', function() { - var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo; + var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo, currentSpec; - function compileInput(inputHtml) { + function compileInput(inputHtml, mockValidity) { inputElm = jqLite(inputHtml); + if (isObject(mockValidity)) { + VALIDITY_STATE_PROPERTY = 'ngMockValidity'; + inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity); + currentSpec.after(function() { + VALIDITY_STATE_PROPERTY = 'validity'; + }); + } formElm = jqLite('<form name="form"></form>'); formElm.append(inputElm); $compile(formElm)(scope); } + var attrs; + beforeEach(function() { currentSpec = this; }); + afterEach(function() { currentSpec = null; }); + beforeEach(module(function($compileProvider) { + $compileProvider.directive('attrCapture', function() { + return function(scope, element, $attrs) { + attrs = $attrs; + }; + }); + })); + beforeEach(inject(function($injector, _$sniffer_, _$browser_) { $sniffer = _$sniffer_; $browser = _$browser_; @@ -844,6 +862,33 @@ describe('input', function() { }); + it('should invalidate number if suffering from bad input', function() { + compileInput('<input type="number" ng-model="age" />', { + valid: false, + badInput: true + }); + + changeInputValueTo('10a'); + expect(scope.age).toBeUndefined(); + expect(inputElm).toBeInvalid(); + }); + + + it('should validate number if transition from bad input to empty string', function() { + var validity = { + valid: false, + badInput: true + }; + compileInput('<input type="number" ng-model="age" />', validity); + changeInputValueTo('10a'); + validity.badInput = false; + validity.valid = true; + changeInputValueTo(''); + expect(scope.age).toBeNull(); + expect(inputElm).toBeValid(); + }); + + describe('min', function() { it('should validate', function() {