fix(input): improve html5 validation support
This CL improves mocking support for HTML5 validation, fixes the behaviour which invokes validators. Previously, an input would only be revalidated if either its value changed, or if it was the empty string but did not suffer from bad input --- now, it will be revalidated if either the value has changed, or the value is the empty string, there is a ValidityState for the element, and that ValidityState is being tested by one of the validators in the pipeline. Closes #7937 Closes #7957
This commit is contained in:
@@ -100,6 +100,7 @@
|
||||
"assertNotHasOwnProperty": false,
|
||||
"getter": false,
|
||||
"getBlockElements": false,
|
||||
"VALIDITY_STATE_PROPERTY": false,
|
||||
|
||||
/* AngularPublic.js */
|
||||
"version": false,
|
||||
|
||||
@@ -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
|
||||
|
||||
+28
-11
@@ -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;
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"assertNotHasOwnProperty": false,
|
||||
"getter": false,
|
||||
"getBlockElements": false,
|
||||
"VALIDITY_STATE_PROPERTY": true,
|
||||
|
||||
/* filters.js */
|
||||
"getFirstThursdayOfYear": false,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user