Skip to content

Commit 69730db

Browse files
committed
fix(input): fix false initial 'number' validation error in input[number] with observed attributes
When no value has been set for an input[number] it's viewValue is undefined. This lead to false parse validation errors, when `ctrl.$validate()` was fired before interacting with the element. A typical situation of this happening is when other directives on the same element `$observe()` attribute values. (Such directives are ngRequired, min, max etc.) More specifically, the parser for native HTML5 validation returns undefined when the input is invalid and returns the value unchanged when the input is valid. Thus, when the value itself is undefined, the NgModelController is tricked into believing there is a native validation error. Closes angular#9106
1 parent d1434c9 commit 69730db

File tree

2 files changed

+129
-14
lines changed

2 files changed

+129
-14
lines changed

src/ng/directive/input.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1161,16 +1161,16 @@ function badInputChecker(scope, element, attr, ctrl) {
11611161
}
11621162

11631163
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1164-
badInputChecker(scope, element, attr, ctrl);
1165-
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1166-
11671164
ctrl.$$parserName = 'number';
11681165
ctrl.$parsers.push(function(value) {
11691166
if (ctrl.$isEmpty(value)) return null;
11701167
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
11711168
return undefined;
11721169
});
11731170

1171+
badInputChecker(scope, element, attr, ctrl);
1172+
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1173+
11741174
ctrl.$formatters.push(function(value) {
11751175
if (!ctrl.$isEmpty(value)) {
11761176
if (!isNumber(value)) {

test/ng/directive/inputSpec.js

+126-11
Original file line numberDiff line numberDiff line change
@@ -1140,10 +1140,8 @@ describe('ngModel', function() {
11401140

11411141
dealoc(element);
11421142
}));
1143-
11441143
});
11451144

1146-
11471145
describe('input', function() {
11481146
var formElm, inputElm, scope, $compile, $sniffer, $browser, changeInputValueTo, currentSpec;
11491147

@@ -2209,7 +2207,6 @@ describe('input', function() {
22092207
compileInput('<input type="text" name="input" ng-model="value" minlength="3" />');
22102208
expect(scope.value).toBe('12345');
22112209
});
2212-
22132210
});
22142211

22152212

@@ -3387,7 +3384,6 @@ describe('input', function() {
33873384
expect(inputElm).toBeInvalid();
33883385
});
33893386

3390-
33913387
it('should render as blank if null', function() {
33923388
compileInput('<input type="number" ng-model="age" />');
33933389

@@ -3408,7 +3404,6 @@ describe('input', function() {
34083404
expect(inputElm.val()).toBe('');
34093405
});
34103406

3411-
34123407
it('should parse empty string to null', function() {
34133408
compileInput('<input type="number" ng-model="age" />');
34143409

@@ -3419,7 +3414,6 @@ describe('input', function() {
34193414
expect(inputElm).toBeValid();
34203415
});
34213416

3422-
34233417
it('should only invalidate the model if suffering from bad input when the data is parsed', function() {
34243418
compileInput('<input type="number" ng-model="age" />', {
34253419
valid: false,
@@ -3435,7 +3429,6 @@ describe('input', function() {
34353429
expect(inputElm).toBeInvalid();
34363430
});
34373431

3438-
34393432
it('should validate number if transition from bad input to empty string', function() {
34403433
var validity = {
34413434
valid: false,
@@ -3450,14 +3443,25 @@ describe('input', function() {
34503443
expect(inputElm).toBeValid();
34513444
});
34523445

3446+
it('should be valid when no value has been set even if it is validated', function() {
3447+
// This situation isn't common, but it does arise, when other directives
3448+
// on the same element observe an attribute (e.g. ngRequired, min, max etc).
3449+
3450+
compileInput('<input type="number" name="alias" ng-model="value" />');
3451+
3452+
scope.form.alias.$validate();
3453+
3454+
expect(inputElm).toBeValid();
3455+
expect(scope.form.alias.$error.number).toBeFalsy();
3456+
});
3457+
34533458
it('should throw if the model value is not a number', function() {
34543459
expect(function() {
34553460
scope.value = 'one';
34563461
compileInput('<input type="number" ng-model="value" />');
34573462
}).toThrowMinErr('ngModel', 'numfmt', "Expected `one` to be a number");
34583463
});
34593464

3460-
34613465
describe('min', function() {
34623466

34633467
it('should validate', function() {
@@ -3540,7 +3544,6 @@ describe('input', function() {
35403544
});
35413545
});
35423546

3543-
35443547
describe('max', function() {
35453548

35463549
it('should validate', function() {
@@ -3623,7 +3626,6 @@ describe('input', function() {
36233626
});
36243627
});
36253628

3626-
36273629
describe('required', function() {
36283630

36293631
it('should be valid even if value is 0', function() {
@@ -3655,6 +3657,120 @@ describe('input', function() {
36553657
});
36563658
});
36573659

3660+
describe('ngRequired', function() {
3661+
3662+
describe('when the ngRequired expression initially evaluates to true', function() {
3663+
3664+
it('should be valid even if value is 0', function() {
3665+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="true" />');
3666+
3667+
changeInputValueTo('0');
3668+
expect(inputElm).toBeValid();
3669+
expect(scope.value).toBe(0);
3670+
expect(scope.form.alias.$error.required).toBeFalsy();
3671+
});
3672+
3673+
it('should be valid even if value 0 is set from model', function() {
3674+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="true" />');
3675+
3676+
scope.$apply('value = 0');
3677+
3678+
expect(inputElm).toBeValid();
3679+
expect(inputElm.val()).toBe('0');
3680+
expect(scope.form.alias.$error.required).toBeFalsy();
3681+
});
3682+
3683+
it('should register required on non boolean elements', function() {
3684+
compileInput('<div ng-model="value" name="alias" ng-required="true">');
3685+
3686+
scope.$apply("value = ''");
3687+
3688+
expect(inputElm).toBeInvalid();
3689+
expect(scope.form.alias.$error.required).toBeTruthy();
3690+
});
3691+
3692+
it('should be invalid when the user enters an invalid value', function() {
3693+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="true" />');
3694+
3695+
changeInputValueTo('Hello, world !');
3696+
expect(inputElm.val()).toBe('');
3697+
expect(inputElm).toBeInvalid();
3698+
expect(scope.form.alias.$error.required).toBeTruthy();
3699+
});
3700+
3701+
it('should change from invalid to valid when the value is empty and the ngRequired expression changes to false', function() {
3702+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="ngRequiredExpr" />');
3703+
3704+
scope.$apply('ngRequiredExpr = true');
3705+
3706+
expect(inputElm).toBeInvalid();
3707+
expect(scope.value).toBeUndefined();
3708+
expect(scope.form.alias.$error.required).toBeTruthy();
3709+
3710+
scope.$apply('ngRequiredExpr = false');
3711+
3712+
expect(inputElm).toBeValid();
3713+
expect(scope.value).toBeNull();
3714+
expect(scope.form.alias.$error.required).toBeFalsy();
3715+
});
3716+
});
3717+
3718+
describe('when the ngRequired expression initially evaluates to false', function() {
3719+
3720+
it('should be valid even if value is empty', function() {
3721+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="false" />');
3722+
3723+
expect(inputElm).toBeValid();
3724+
expect(scope.value).toBeNull();
3725+
expect(scope.form.alias.$error.required).toBeFalsy();
3726+
expect(scope.form.alias.$error.number).toBeFalsy();
3727+
});
3728+
3729+
it('should be valid if value is non-empty', function() {
3730+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="false" />');
3731+
3732+
changeInputValueTo('42');
3733+
expect(inputElm).toBeValid();
3734+
expect(scope.value).toBe(42);
3735+
expect(scope.form.alias.$error.required).toBeFalsy();
3736+
});
3737+
3738+
it('should not have the "required" error flag set even if the value is not a number', function() {
3739+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="false" />');
3740+
3741+
changeInputValueTo('Hello, world !');
3742+
expect(inputElm.val()).toBe('');
3743+
expect(inputElm).toBeValid();
3744+
expect(scope.form.alias.$error.required).toBeFalsy();
3745+
});
3746+
3747+
it('should not register required on non boolean elements', function() {
3748+
compileInput('<div ng-model="value" name="alias" ng-required="false">');
3749+
3750+
scope.$apply("value = ''");
3751+
3752+
expect(inputElm).toBeValid();
3753+
expect(scope.form.alias.$error.required).toBeFalsy();
3754+
});
3755+
3756+
it('should change from valid to invalid when the value is empty and the ngRequired expression changes to true', function() {
3757+
compileInput('<input type="number" ng-model="value" name="alias" ng-required="ngRequiredExpr" />');
3758+
3759+
scope.$apply('ngRequiredExpr = false');
3760+
3761+
expect(inputElm).toBeValid();
3762+
expect(scope.value).toBeNull();
3763+
expect(scope.form.alias.$error.required).toBeFalsy();
3764+
3765+
scope.$apply('ngRequiredExpr = true');
3766+
3767+
expect(inputElm).toBeInvalid();
3768+
expect(scope.value).toBeUndefined();
3769+
expect(scope.form.alias.$error.required).toBeTruthy();
3770+
});
3771+
});
3772+
});
3773+
36583774
describe('minlength', function() {
36593775

36603776
it('should invalidate values that are shorter than the given minlength', function() {
@@ -3700,7 +3816,6 @@ describe('input', function() {
37003816
});
37013817
});
37023818

3703-
37043819
describe('maxlength', function() {
37053820

37063821
it('should invalidate values that are longer than the given maxlength', function() {

0 commit comments

Comments
 (0)