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

fix(input): improve html5 validation support (v1.2.x) #7957

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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