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

Commit db044c4

Browse files
committed
fix(ngModel): treat undefined parse responses as parse errors
With this commit, ngModel will now handle parsing first and then validation afterwards once the parsing is successful. If any parser along the way returns `undefined` then ngModel will break the chain of parsing and register a a parser error represented by the type of input that is being collected (e.g. number, date, datetime, url, etc...). If a parser fails for a standard text input field then an error of `parse` will be placed on `model.$error`. BREAKING CHANGE Any parser code from before that returned an `undefined` value (or nothing at all) will now cause a parser failure. When this occurs none of the validators present in `$validators` will run until the parser error is gone.
1 parent 0e44ac2 commit db044c4

File tree

3 files changed

+264
-104
lines changed

3 files changed

+264
-104
lines changed

src/ng/directive/form.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ var nullFormCtrl = {
77
$setValidity: noop,
88
$setDirty: noop,
99
$setPristine: noop,
10-
$setSubmitted: noop
10+
$setSubmitted: noop,
11+
$$clearControlValidity: noop
1112
},
1213
SUBMITTED_CLASS = 'ng-submitted';
1314

@@ -144,11 +145,15 @@ function FormController(element, attrs, $scope, $animate) {
144145
if (control.$name && form[control.$name] === control) {
145146
delete form[control.$name];
146147
}
148+
149+
form.$$clearControlValidity(control);
150+
arrayRemove(controls, control);
151+
};
152+
153+
form.$$clearControlValidity = function(control) {
147154
forEach(errors, function(queue, validationToken) {
148155
form.$setValidity(validationToken, true, control);
149156
});
150-
151-
arrayRemove(controls, control);
152157
};
153158

154159
/**

src/ng/directive/input.js

+75-91
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
1818
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d))?$/;
1919
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
2020

21+
var $ngModelMinErr = new minErr('ngModel');
2122
var inputType = {
2223

2324
/**
@@ -885,13 +886,6 @@ var inputType = {
885886
'file': noop
886887
};
887888

888-
// A helper function to call $setValidity and return the value / undefined,
889-
// a pattern that is repeated a lot in the input validation logic.
890-
function validate(ctrl, validatorName, validity, value){
891-
ctrl.$setValidity(validatorName, validity);
892-
return validity ? value : undefined;
893-
}
894-
895889
function testFlags(validity, flags) {
896890
var i, flag;
897891
if (flags) {
@@ -905,25 +899,6 @@ function testFlags(validity, flags) {
905899
return false;
906900
}
907901

908-
// Pass validity so that behaviour can be mocked easier.
909-
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
910-
if (isObject(validity)) {
911-
ctrl.$$hasNativeValidators = true;
912-
var validator = function(value) {
913-
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
914-
// perform the required validation)
915-
if (!ctrl.$error[validatorName] &&
916-
!testFlags(validity, ignoreFlags) &&
917-
testFlags(validity, badFlags)) {
918-
ctrl.$setValidity(validatorName, false);
919-
return;
920-
}
921-
return value;
922-
};
923-
ctrl.$parsers.push(validator);
924-
}
925-
}
926-
927902
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
928903
var validity = element.prop(VALIDITY_STATE_PROPERTY);
929904
var placeholder = element[0].placeholder, noevent = {};
@@ -1074,25 +1049,20 @@ function createDateParser(regexp, mapping) {
10741049

10751050
function createDateInputType(type, regexp, parseDate, format) {
10761051
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
1052+
badInputChecker(scope, element, attr, ctrl);
10771053
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
10781054
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
10791055

1056+
ctrl.$$parserName = type;
10801057
ctrl.$parsers.push(function(value) {
1081-
if(ctrl.$isEmpty(value)) {
1082-
ctrl.$setValidity(type, true);
1083-
return null;
1084-
}
1085-
1086-
if(regexp.test(value)) {
1087-
ctrl.$setValidity(type, true);
1058+
if (ctrl.$isEmpty(value)) return null;
1059+
if (regexp.test(value)) {
10881060
var parsedDate = parseDate(value);
10891061
if (timezone === 'UTC') {
10901062
parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset());
10911063
}
10921064
return parsedDate;
10931065
}
1094-
1095-
ctrl.$setValidity(type, false);
10961066
return undefined;
10971067
});
10981068

@@ -1104,90 +1074,80 @@ function createDateInputType(type, regexp, parseDate, format) {
11041074
});
11051075

11061076
if(attr.min) {
1107-
var minValidator = function(value) {
1108-
var valid = ctrl.$isEmpty(value) ||
1109-
(parseDate(value) >= parseDate(attr.min));
1110-
ctrl.$setValidity('min', valid);
1111-
return valid ? value : undefined;
1112-
};
1113-
1114-
ctrl.$parsers.push(minValidator);
1115-
ctrl.$formatters.push(minValidator);
1077+
ctrl.$validators.min = function(value) {
1078+
return ctrl.$isEmpty(value) || isUndefined(attr.min) || parseDate(value) >= parseDate(attr.min);
1079+
};
11161080
}
11171081

11181082
if(attr.max) {
1119-
var maxValidator = function(value) {
1120-
var valid = ctrl.$isEmpty(value) ||
1121-
(parseDate(value) <= parseDate(attr.max));
1122-
ctrl.$setValidity('max', valid);
1123-
return valid ? value : undefined;
1124-
};
1125-
1126-
ctrl.$parsers.push(maxValidator);
1127-
ctrl.$formatters.push(maxValidator);
1083+
ctrl.$validators.max = function(value) {
1084+
return ctrl.$isEmpty(value) || isUndefined(attr.max) || parseDate(value) <= parseDate(attr.max);
1085+
};
11281086
}
11291087
};
11301088
}
11311089

1132-
var numberBadFlags = ['badInput'];
1090+
function badInputChecker(scope, element, attr, ctrl) {
1091+
var node = element[0];
1092+
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
1093+
if (nativeValidation) {
1094+
ctrl.$parsers.push(function(value) {
1095+
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
1096+
return validity.badInput || validity.typeMismatch ? undefined : value;
1097+
});
1098+
}
1099+
}
11331100

11341101
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1102+
badInputChecker(scope, element, attr, ctrl);
11351103
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11361104

1105+
ctrl.$$parserName = 'number';
11371106
ctrl.$parsers.push(function(value) {
1138-
var empty = ctrl.$isEmpty(value);
1139-
if (empty || NUMBER_REGEXP.test(value)) {
1140-
ctrl.$setValidity('number', true);
1141-
return value === '' ? null : (empty ? value : parseFloat(value));
1142-
} else {
1143-
ctrl.$setValidity('number', false);
1144-
return undefined;
1145-
}
1107+
if(ctrl.$isEmpty(value)) return null;
1108+
if(NUMBER_REGEXP.test(value)) return parseFloat(value);
1109+
return undefined;
11461110
});
11471111

1148-
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
1149-
11501112
ctrl.$formatters.push(function(value) {
1151-
return ctrl.$isEmpty(value) ? '' : '' + value;
1113+
if (!ctrl.$isEmpty(value)) {
1114+
if (!isNumber(value)) {
1115+
throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
1116+
}
1117+
value = value.toString();
1118+
}
1119+
return value;
11521120
});
11531121

11541122
if (attr.min) {
1155-
var minValidator = function(value) {
1156-
var min = parseFloat(attr.min);
1157-
return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
1123+
ctrl.$validators.min = function(value) {
1124+
return ctrl.$isEmpty(value) || isUndefined(attr.min) || value >= parseFloat(attr.min);
11581125
};
1159-
1160-
ctrl.$parsers.push(minValidator);
1161-
ctrl.$formatters.push(minValidator);
11621126
}
11631127

11641128
if (attr.max) {
1165-
var maxValidator = function(value) {
1166-
var max = parseFloat(attr.max);
1167-
return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
1129+
ctrl.$validators.max = function(value) {
1130+
return ctrl.$isEmpty(value) || isUndefined(attr.max) || value <= parseFloat(attr.max);
11681131
};
1169-
1170-
ctrl.$parsers.push(maxValidator);
1171-
ctrl.$formatters.push(maxValidator);
11721132
}
1173-
1174-
ctrl.$formatters.push(function(value) {
1175-
return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
1176-
});
11771133
}
11781134

11791135
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1136+
badInputChecker(scope, element, attr, ctrl);
11801137
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11811138

1139+
ctrl.$$parserName = 'url';
11821140
ctrl.$validators.url = function(modelValue, viewValue) {
11831141
var value = modelValue || viewValue;
11841142
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
11851143
};
11861144
}
11871145

11881146
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1147+
badInputChecker(scope, element, attr, ctrl);
11891148
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11901149

1150+
ctrl.$$parserName = 'email';
11911151
ctrl.$validators.email = function(modelValue, viewValue) {
11921152
var value = modelValue || viewValue;
11931153
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
@@ -1223,7 +1183,7 @@ function parseConstantExpr($parse, context, name, expression, fallback) {
12231183
if (isDefined(expression)) {
12241184
parseFn = $parse(expression);
12251185
if (!parseFn.constant) {
1226-
throw new minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
1186+
throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
12271187
'`{1}`.', name, expression);
12281188
}
12291189
return parseFn(context);
@@ -1598,7 +1558,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15981558
ctrl = this;
15991559

16001560
if (!ngModelSet) {
1601-
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
1561+
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
16021562
$attr.ngModel, startingTag($element));
16031563
}
16041564

@@ -1663,6 +1623,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16631623
$animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
16641624
}
16651625

1626+
this.$$clearValidity = function() {
1627+
forEach(ctrl.$error, function(val, key) {
1628+
var validationKey = snake_case(key, '-');
1629+
$animate.removeClass($element, VALID_CLASS + validationKey);
1630+
$animate.removeClass($element, INVALID_CLASS + validationKey);
1631+
});
1632+
1633+
invalidCount = 0;
1634+
$error = ctrl.$error = {};
1635+
1636+
parentForm.$$clearControlValidity(ctrl);
1637+
};
1638+
16661639
/**
16671640
* @ngdoc method
16681641
* @name ngModel.NgModelController#$setValidity
@@ -1694,7 +1667,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16941667
ctrl.$valid = true;
16951668
ctrl.$invalid = false;
16961669
}
1697-
} else {
1670+
} else if(!$error[validationErrorKey]) {
16981671
toggleValidCss(false);
16991672
ctrl.$invalid = true;
17001673
ctrl.$valid = false;
@@ -1883,16 +1856,27 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18831856
parentForm.$setDirty();
18841857
}
18851858

1886-
var modelValue = viewValue;
1887-
forEach(ctrl.$parsers, function(fn) {
1888-
modelValue = fn(modelValue);
1889-
});
1859+
var hasBadInput, modelValue = viewValue;
1860+
for(var i = 0; i < ctrl.$parsers.length; i++) {
1861+
modelValue = ctrl.$parsers[i](modelValue);
1862+
if(isUndefined(modelValue)) {
1863+
hasBadInput = true;
1864+
break;
1865+
}
1866+
}
18901867

1891-
if (ctrl.$modelValue !== modelValue &&
1892-
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
1868+
var parserName = ctrl.$$parserName || 'parse';
1869+
if (hasBadInput) {
1870+
ctrl.$$invalidModelValue = ctrl.$modelValue = undefined;
1871+
ctrl.$$clearValidity();
1872+
ctrl.$setValidity(parserName, false);
1873+
} else if (ctrl.$modelValue !== modelValue &&
1874+
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
1875+
ctrl.$setValidity(parserName, true);
18931876
ctrl.$$runValidators(modelValue, viewValue);
1894-
ctrl.$$writeModelToScope();
18951877
}
1878+
1879+
ctrl.$$writeModelToScope();
18961880
};
18971881

18981882
this.$$writeModelToScope = function() {

0 commit comments

Comments
 (0)