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

Commit

Permalink
fix(input): improve html5 validation support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
caitp committed Jun 24, 2014
1 parent e5f454c commit ab2e83c
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"assertNotHasOwnProperty": false,
"getter": false,
"getBlockElements": false,
"VALIDITY_STATE_PROPERTY": false,

/* AngularPublic.js */
"version": false,
Expand Down
5 changes: 5 additions & 0 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
-angularModule,
-nodeName_,
-uid,
-VALIDITY_STATE_PROPERTY,
-lowercase,
-uppercase,
Expand Down Expand Up @@ -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
Expand Down
39 changes: 28 additions & 11 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"assertNotHasOwnProperty": false,
"getter": false,
"getBlockElements": false,
"VALIDITY_STATE_PROPERTY": true,

/* filters.js */
"getFirstThursdayOfYear": false,
Expand Down
49 changes: 47 additions & 2 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit ab2e83c

Please sign in to comment.