Skip to content

Commit 5aa7600

Browse files
fix(input): false is no longer an empty value by default
`checkboxInputType` and `ngList` directives need to have special logic for whether they are empty or not. Previously this had been hard coded into their own directives or the `ngRequired` directive. This made it difficult to handle these special cases. This change factors out the question of whether an input is empty into a method `$isEmpty` on the `ngModelController`. The `ngRequired` directive now uses this method when testing for validity and directives, such as `checkbox` or `ngList` can override it to apply logic specific to their needs. Closes angular#3490, angular#3658, angular#2594
1 parent f8f8f75 commit 5aa7600

File tree

3 files changed

+102
-16
lines changed

3 files changed

+102
-16
lines changed

src/ng/directive/input.js

+46-14
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
445445

446446

447447
ctrl.$render = function() {
448-
element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
448+
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
449449
};
450450

451451
// pattern validator
@@ -454,7 +454,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
454454
match;
455455

456456
var validate = function(regexp, value) {
457-
if (isEmpty(value) || regexp.test(value)) {
457+
if (ctrl.$isEmpty(value) || regexp.test(value)) {
458458
ctrl.$setValidity('pattern', true);
459459
return value;
460460
} else {
@@ -468,7 +468,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
468468
if (match) {
469469
pattern = new RegExp(match[1], match[2]);
470470
patternValidator = function(value) {
471-
return validate(pattern, value)
471+
return validate(pattern, value);
472472
};
473473
} else {
474474
patternValidator = function(value) {
@@ -491,7 +491,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
491491
if (attr.ngMinlength) {
492492
var minlength = int(attr.ngMinlength);
493493
var minLengthValidator = function(value) {
494-
if (!isEmpty(value) && value.length < minlength) {
494+
if (!ctrl.$isEmpty(value) && value.length < minlength) {
495495
ctrl.$setValidity('minlength', false);
496496
return undefined;
497497
} else {
@@ -508,7 +508,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
508508
if (attr.ngMaxlength) {
509509
var maxlength = int(attr.ngMaxlength);
510510
var maxLengthValidator = function(value) {
511-
if (!isEmpty(value) && value.length > maxlength) {
511+
if (!ctrl.$isEmpty(value) && value.length > maxlength) {
512512
ctrl.$setValidity('maxlength', false);
513513
return undefined;
514514
} else {
@@ -526,7 +526,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
526526
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
527527

528528
ctrl.$parsers.push(function(value) {
529-
var empty = isEmpty(value);
529+
var empty = ctrl.$isEmpty(value);
530530
if (empty || NUMBER_REGEXP.test(value)) {
531531
ctrl.$setValidity('number', true);
532532
return value === '' ? null : (empty ? value : parseFloat(value));
@@ -537,13 +537,13 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
537537
});
538538

539539
ctrl.$formatters.push(function(value) {
540-
return isEmpty(value) ? '' : '' + value;
540+
return ctrl.$isEmpty(value) ? '' : '' + value;
541541
});
542542

543543
if (attr.min) {
544544
var min = parseFloat(attr.min);
545545
var minValidator = function(value) {
546-
if (!isEmpty(value) && value < min) {
546+
if (!ctrl.$isEmpty(value) && value < min) {
547547
ctrl.$setValidity('min', false);
548548
return undefined;
549549
} else {
@@ -559,7 +559,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
559559
if (attr.max) {
560560
var max = parseFloat(attr.max);
561561
var maxValidator = function(value) {
562-
if (!isEmpty(value) && value > max) {
562+
if (!ctrl.$isEmpty(value) && value > max) {
563563
ctrl.$setValidity('max', false);
564564
return undefined;
565565
} else {
@@ -574,7 +574,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
574574

575575
ctrl.$formatters.push(function(value) {
576576

577-
if (isEmpty(value) || isNumber(value)) {
577+
if (ctrl.$isEmpty(value) || isNumber(value)) {
578578
ctrl.$setValidity('number', true);
579579
return value;
580580
} else {
@@ -588,7 +588,7 @@ function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
588588
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
589589

590590
var urlValidator = function(value) {
591-
if (isEmpty(value) || URL_REGEXP.test(value)) {
591+
if (ctrl.$isEmpty(value) || URL_REGEXP.test(value)) {
592592
ctrl.$setValidity('url', true);
593593
return value;
594594
} else {
@@ -605,7 +605,7 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
605605
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
606606

607607
var emailValidator = function(value) {
608-
if (isEmpty(value) || EMAIL_REGEXP.test(value)) {
608+
if (ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value)) {
609609
ctrl.$setValidity('email', true);
610610
return value;
611611
} else {
@@ -657,6 +657,12 @@ function checkboxInputType(scope, element, attr, ctrl) {
657657
element[0].checked = ctrl.$viewValue;
658658
};
659659

660+
// Override the standard `$isEmpty` because a value of `false` means empty in a checkbox.
661+
var _$isEmpty = ctrl.$isEmpty;
662+
ctrl.$isEmpty = function(value) {
663+
return _$isEmpty(value) || value === false;
664+
};
665+
660666
ctrl.$formatters.push(function(value) {
661667
return value === trueValue;
662668
});
@@ -992,6 +998,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
992998
*/
993999
this.$render = noop;
9941000

1001+
/**
1002+
* @ngdoc function
1003+
* @name { ng.directive:ngModel.NgModelController#$isEmpty
1004+
* @methodOf ng.directive:ngModel.NgModelController
1005+
*
1006+
* @description
1007+
* This is called when we need to determine if the value of the input is empty.
1008+
*
1009+
* For instance, the required directive does this to work out if the input has data or not.
1010+
* The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
1011+
*
1012+
* You can override this for input directives whose concept of being empty is different to the
1013+
* default. The `checkboxInputType` directive does this because in its case a value of `false`
1014+
* implies empty.
1015+
*/
1016+
this.$isEmpty = isEmpty;
1017+
9951018
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
9961019
invalidCount = 0, // used to easily determine if we are valid
9971020
$error = this.$error = {}; // keep invalid keys here
@@ -1266,7 +1289,7 @@ var requiredDirective = function() {
12661289
attr.required = true; // force truthy in case we are on non input element
12671290

12681291
var validator = function(value) {
1269-
if (attr.required && (isEmpty(value) || value === false)) {
1292+
if (attr.required && ctrl.$isEmpty(value)) {
12701293
ctrl.$setValidity('required', false);
12711294
return;
12721295
} else {
@@ -1326,7 +1349,7 @@ var requiredDirective = function() {
13261349
13271350
it('should be invalid if empty', function() {
13281351
input('names').enter('');
1329-
expect(binding('names')).toEqual('[]');
1352+
expect(binding('names')).toEqual('');
13301353
expect(binding('myForm.namesInput.$valid')).toEqual('false');
13311354
expect(element('span.error').css('display')).not().toBe('none');
13321355
});
@@ -1341,6 +1364,9 @@ var ngListDirective = function() {
13411364
separator = match && new RegExp(match[1]) || attr.ngList || ',';
13421365

13431366
var parse = function(viewValue) {
1367+
// If the viewValue is invalid (say required but empty) it will be `undefined`
1368+
if ( isUndefined(viewValue) ) return;
1369+
13441370
var list = [];
13451371

13461372
if (viewValue) {
@@ -1360,6 +1386,12 @@ var ngListDirective = function() {
13601386

13611387
return undefined;
13621388
});
1389+
1390+
// Override the standard $isEmpty because an empty array means the input is empty
1391+
var _$isEmpty = ctrl.$isEmpty;
1392+
ctrl.$isEmpty = function(value) {
1393+
return _$isEmpty(value) || value.length === 0;
1394+
};
13631395
}
13641396
};
13651397
};

test/ng/directive/inputSpec.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,16 @@ describe('input', function() {
998998
expect(scope.list).toEqual([]);
999999
});
10001000

1001+
it('should be invalid if required and empty', function() {
1002+
compileInput('<input type="text" ng-list ng-model="list" required>');
1003+
changeInputValueTo('');
1004+
expect(scope.list).toBeUndefined();
1005+
expect(inputElm).toBeInvalid();
1006+
changeInputValueTo('a,b');
1007+
expect(scope.list).toEqual(['a','b']);
1008+
expect(inputElm).toBeValid();
1009+
});
1010+
10011011

10021012
it('should allow custom separator', function() {
10031013
compileInput('<input type="text" ng-model="list" ng-list=":" />');
@@ -1090,10 +1100,29 @@ describe('input', function() {
10901100

10911101

10921102
it('should set $invalid when model undefined', function() {
1093-
compileInput('<input type="text" ng-model="notDefiend" required />');
1103+
compileInput('<input type="text" ng-model="notDefined" required />');
10941104
scope.$digest();
10951105
expect(inputElm).toBeInvalid();
1096-
})
1106+
});
1107+
1108+
1109+
it('should allow `false` as a valid value when the input type is not "checkbox"', function() {
1110+
compileInput('<input type="radio" ng-value="true" ng-model="answer" required />' +
1111+
'<input type="radio" ng-value="false" ng-model="answer" required />');
1112+
1113+
scope.$apply();
1114+
expect(inputElm).toBeInvalid();
1115+
1116+
scope.$apply(function() {
1117+
scope.answer = true;
1118+
});
1119+
expect(inputElm).toBeValid();
1120+
1121+
scope.$apply(function() {
1122+
scope.answer = false;
1123+
});
1124+
expect(inputElm).toBeValid();
1125+
});
10971126
});
10981127

10991128

test/ng/directive/selectSpec.js

+25
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,31 @@ describe('select', function() {
12131213
});
12141214
expect(element).toBeValid();
12151215
});
1216+
1217+
1218+
it('should allow falsy values as values', function() {
1219+
createSelect({
1220+
'ng-model': 'value',
1221+
'ng-options': 'item.value as item.name for item in values',
1222+
'ng-required': 'required'
1223+
}, true);
1224+
1225+
scope.$apply(function() {
1226+
scope.values = [{name: 'True', value: true}, {name: 'False', value: false}];
1227+
scope.required = false;
1228+
});
1229+
1230+
element.val('1');
1231+
browserTrigger(element, 'change');
1232+
expect(element).toBeValid();
1233+
expect(scope.value).toBe(false);
1234+
1235+
scope.$apply(function() {
1236+
scope.required = true;
1237+
});
1238+
expect(element).toBeValid();
1239+
expect(scope.value).toBe(false);
1240+
});
12161241
});
12171242
});
12181243

0 commit comments

Comments
 (0)