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

Commit 1f6a5a1

Browse files
committed
fix(input): improve html5 validation support
This CL improves mocking support for HTML5 validation, and ensures that it works correctly along with debounced commission of view values. Closes #7936 Closes #7937
1 parent deb008d commit 1f6a5a1

File tree

5 files changed

+82
-22
lines changed

5 files changed

+82
-22
lines changed

src/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"assertNotHasOwnProperty": false,
102102
"getter": false,
103103
"getBlockElements": false,
104+
"VALIDITY_STATE_PROPERTY": false,
104105

105106
/* filters.js */
106107
"getFirstThursdayOfYear": false,

src/Angular.js

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
-nodeName_,
1515
-uid,
1616
-REGEX_STRING_REGEXP,
17+
-VALIDITY_STATE_PROPERTY,
1718
1819
-lowercase,
1920
-uppercase,
@@ -105,6 +106,10 @@
105106

106107
var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
107108

109+
// The name of a form control's ValidityState property.
110+
// This is used so that it's possible for internal tests to create mock ValidityStates.
111+
var VALIDITY_STATE_PROPERTY = 'validity';
112+
108113
/**
109114
* @ngdoc function
110115
* @name angular.lowercase

src/ng/directive/input.js

+37-20
Original file line numberDiff line numberDiff line change
@@ -862,15 +862,29 @@ function validate(ctrl, validatorName, validity, value){
862862
return validity ? value : undefined;
863863
}
864864

865+
function testFlags(validity, flags) {
866+
var i, flag;
867+
if (flags) {
868+
for (i=0; i<flags.length; ++i) {
869+
flag = flags[i];
870+
if (validity[flag]) {
871+
return true;
872+
}
873+
}
874+
}
875+
return false;
876+
}
865877

866-
function addNativeHtml5Validators(ctrl, validatorName, element) {
867-
var validity = element.prop('validity');
878+
// Pass validity so that behaviour can be mocked easier.
879+
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
868880
if (isObject(validity)) {
881+
ctrl.$$hasNativeValidators = true;
869882
var validator = function(value) {
870883
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
871884
// perform the required validation)
872-
if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError ||
873-
validity.typeMismatch) && !validity.valueMissing) {
885+
if (!ctrl.$error[validatorName] &&
886+
!testFlags(validity, ignoreFlags) &&
887+
testFlags(validity, badFlags)) {
874888
ctrl.$setValidity(validatorName, false);
875889
return;
876890
}
@@ -881,8 +895,9 @@ function addNativeHtml5Validators(ctrl, validatorName, element) {
881895
}
882896

883897
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
884-
var validity = element.prop('validity');
898+
var validity = element.prop(VALIDITY_STATE_PROPERTY);
885899
var placeholder = element[0].placeholder, noevent = {};
900+
ctrl.$$validityState = validity;
886901

887902
// In composition mode, users are still inputing intermediate text buffer,
888903
// hold the listener until composition is done.
@@ -921,16 +936,16 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
921936
value = trim(value);
922937
}
923938

924-
if (ctrl.$viewValue !== value ||
925-
// If the value is still empty/falsy, and there is no `required` error, run validators
926-
// again. This enables HTML5 constraint validation errors to affect Angular validation
927-
// even when the first character entered causes an error.
928-
(validity && value === '' && !validity.valueMissing)) {
939+
// If a control is suffering from bad input, browsers discard its value, so it may be
940+
// necessary to revalidate even if the control's value is the same empty value twice in
941+
// a row.
942+
var revalidate = validity && ctrl.$$hasNativeValidators;
943+
if (ctrl.$viewValue !== value || (value === '' && revalidate)) {
929944
if (scope.$$phase) {
930-
ctrl.$setViewValue(value, event);
945+
ctrl.$setViewValue(value, event, revalidate);
931946
} else {
932947
scope.$apply(function() {
933-
ctrl.$setViewValue(value, event);
948+
ctrl.$setViewValue(value, event, revalidate);
934949
});
935950
}
936951
}
@@ -1079,6 +1094,8 @@ function createDateInputType(type, regexp, parseDate, format) {
10791094
};
10801095
}
10811096

1097+
var numberBadFlags = ['badInput'];
1098+
10821099
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
10831100
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
10841101

@@ -1093,7 +1110,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
10931110
}
10941111
});
10951112

1096-
addNativeHtml5Validators(ctrl, 'number', element);
1113+
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
10971114

10981115
ctrl.$formatters.push(function(value) {
10991116
return ctrl.$isEmpty(value) ? '' : '' + value;
@@ -1773,11 +1790,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17731790
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
17741791
* usually handles calling this in response to input events.
17751792
*/
1776-
this.$commitViewValue = function() {
1793+
this.$commitViewValue = function(revalidate) {
17771794
var viewValue = ctrl.$viewValue;
17781795

17791796
$timeout.cancel(pendingDebounce);
1780-
if (ctrl.$$lastCommittedViewValue === viewValue) {
1797+
if (!revalidate && ctrl.$$lastCommittedViewValue === viewValue) {
17811798
return;
17821799
}
17831800
ctrl.$$lastCommittedViewValue = viewValue;
@@ -1842,14 +1859,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18421859
* @param {string} value Value from the view.
18431860
* @param {string} trigger Event that triggered the update.
18441861
*/
1845-
this.$setViewValue = function(value, trigger) {
1862+
this.$setViewValue = function(value, trigger, revalidate) {
18461863
ctrl.$viewValue = value;
18471864
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
1848-
ctrl.$$debounceViewValueCommit(trigger);
1865+
ctrl.$$debounceViewValueCommit(trigger, revalidate);
18491866
}
18501867
};
18511868

1852-
this.$$debounceViewValueCommit = function(trigger) {
1869+
this.$$debounceViewValueCommit = function(trigger, revalidate) {
18531870
var debounceDelay = 0,
18541871
options = ctrl.$options,
18551872
debounce;
@@ -1868,10 +1885,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18681885
$timeout.cancel(pendingDebounce);
18691886
if (debounceDelay) {
18701887
pendingDebounce = $timeout(function() {
1871-
ctrl.$commitViewValue();
1888+
ctrl.$commitViewValue(revalidate);
18721889
}, debounceDelay);
18731890
} else {
1874-
ctrl.$commitViewValue();
1891+
ctrl.$commitViewValue(revalidate);
18751892
}
18761893
};
18771894

test/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"assertNotHasOwnProperty": false,
102102
"getter": false,
103103
"getBlockElements": false,
104+
"VALIDITY_STATE_PROPERTY": true,
104105

105106
/* AngularPublic.js */
106107
"version": false,

test/ng/directive/inputSpec.js

+38-2
Original file line numberDiff line numberDiff line change
@@ -608,17 +608,26 @@ describe('ngModel', function() {
608608

609609

610610
describe('input', function() {
611-
var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo;
611+
var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo, currentSpec;
612612

613-
function compileInput(inputHtml) {
613+
function compileInput(inputHtml, mockValidity) {
614614
inputElm = jqLite(inputHtml);
615+
if (isObject(mockValidity)) {
616+
VALIDITY_STATE_PROPERTY = 'ngMockValidity';
617+
inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity);
618+
currentSpec.after(function() {
619+
VALIDITY_STATE_PROPERTY = 'validity';
620+
});
621+
}
615622
formElm = jqLite('<form name="form"></form>');
616623
formElm.append(inputElm);
617624
$compile(formElm)(scope);
618625
scope.$digest();
619626
}
620627

621628
var attrs;
629+
beforeEach(function() { currentSpec = this; });
630+
afterEach(function() { currentSpec = null; });
622631
beforeEach(module(function($compileProvider) {
623632
$compileProvider.directive('attrCapture', function() {
624633
return function(scope, element, $attrs) {
@@ -2238,6 +2247,33 @@ describe('input', function() {
22382247
});
22392248

22402249

2250+
it('should invalidate number if suffering from bad input', function() {
2251+
compileInput('<input type="number" ng-model="age" />', {
2252+
valid: false,
2253+
badInput: true
2254+
});
2255+
2256+
changeInputValueTo('10a');
2257+
expect(scope.age).toBeUndefined();
2258+
expect(inputElm).toBeInvalid();
2259+
});
2260+
2261+
2262+
it('should validate number if transition from bad input to empty string', function() {
2263+
var validity = {
2264+
valid: false,
2265+
badInput: true
2266+
};
2267+
compileInput('<input type="number" ng-model="age" />', validity);
2268+
changeInputValueTo('10a');
2269+
validity.badInput = false;
2270+
validity.valid = true;
2271+
changeInputValueTo('');
2272+
expect(scope.age).toBeNull();
2273+
expect(inputElm).toBeValid();
2274+
});
2275+
2276+
22412277
describe('min', function() {
22422278

22432279
it('should validate', function() {

0 commit comments

Comments
 (0)