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

Commit 171c1c1

Browse files
committed
fix(ngModel): treat undefined parse responses as parse errors
With this commit, ngModel will now handle parsing first and the validation when dealing with the the view value. 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 23da614 commit 171c1c1

File tree

3 files changed

+264
-106
lines changed

3 files changed

+264
-106
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-93
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)$/;
1919
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
2020

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

2324
/**
@@ -870,13 +871,6 @@ var inputType = {
870871
'file': noop
871872
};
872873

873-
// A helper function to call $setValidity and return the value / undefined,
874-
// a pattern that is repeated a lot in the input validation logic.
875-
function validate(ctrl, validatorName, validity, value){
876-
ctrl.$setValidity(validatorName, validity);
877-
return validity ? value : undefined;
878-
}
879-
880874
function testFlags(validity, flags) {
881875
var i, flag;
882876
if (flags) {
@@ -890,25 +884,6 @@ function testFlags(validity, flags) {
890884
return false;
891885
}
892886

893-
// Pass validity so that behaviour can be mocked easier.
894-
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
895-
if (isObject(validity)) {
896-
ctrl.$$hasNativeValidators = true;
897-
var validator = function(value) {
898-
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
899-
// perform the required validation)
900-
if (!ctrl.$error[validatorName] &&
901-
!testFlags(validity, ignoreFlags) &&
902-
testFlags(validity, badFlags)) {
903-
ctrl.$setValidity(validatorName, false);
904-
return;
905-
}
906-
return value;
907-
};
908-
ctrl.$parsers.push(validator);
909-
}
910-
}
911-
912887
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
913888
var validity = element.prop(VALIDITY_STATE_PROPERTY);
914889
var placeholder = element[0].placeholder, noevent = {};
@@ -1060,20 +1035,13 @@ function createDateParser(regexp, mapping) {
10601035

10611036
function createDateInputType(type, regexp, parseDate, format) {
10621037
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
1038+
badInputChecker(scope, element, attr, ctrl);
10631039
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
10641040

1041+
ctrl.$$parserName = type;
10651042
ctrl.$parsers.push(function(value) {
1066-
if(ctrl.$isEmpty(value)) {
1067-
ctrl.$setValidity(type, true);
1068-
return null;
1069-
}
1070-
1071-
if(regexp.test(value)) {
1072-
ctrl.$setValidity(type, true);
1073-
return parseDate(value);
1074-
}
1075-
1076-
ctrl.$setValidity(type, false);
1043+
if(ctrl.$isEmpty(value)) return null;
1044+
if(regexp.test(value)) return parseDate(value);
10771045
return undefined;
10781046
});
10791047

@@ -1085,90 +1053,80 @@ function createDateInputType(type, regexp, parseDate, format) {
10851053
});
10861054

10871055
if(attr.min) {
1088-
var minValidator = function(value) {
1089-
var valid = ctrl.$isEmpty(value) ||
1090-
(parseDate(value) >= parseDate(attr.min));
1091-
ctrl.$setValidity('min', valid);
1092-
return valid ? value : undefined;
1093-
};
1094-
1095-
ctrl.$parsers.push(minValidator);
1096-
ctrl.$formatters.push(minValidator);
1056+
ctrl.$validators.min = function(value) {
1057+
return ctrl.$isEmpty(value) || isUndefined(attr.min) || parseDate(value) >= parseDate(attr.min);
1058+
};
10971059
}
10981060

10991061
if(attr.max) {
1100-
var maxValidator = function(value) {
1101-
var valid = ctrl.$isEmpty(value) ||
1102-
(parseDate(value) <= parseDate(attr.max));
1103-
ctrl.$setValidity('max', valid);
1104-
return valid ? value : undefined;
1105-
};
1106-
1107-
ctrl.$parsers.push(maxValidator);
1108-
ctrl.$formatters.push(maxValidator);
1062+
ctrl.$validators.max = function(value) {
1063+
return ctrl.$isEmpty(value) || isUndefined(attr.max) || parseDate(value) <= parseDate(attr.max);
1064+
};
11091065
}
11101066
};
11111067
}
11121068

1113-
var numberBadFlags = ['badInput'];
1069+
function badInputChecker(scope, element, attr, ctrl) {
1070+
var node = element[0];
1071+
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
1072+
if (nativeValidation) {
1073+
ctrl.$parsers.push(function(value) {
1074+
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
1075+
return validity.badInput || validity.typeMismatch ? undefined : value;
1076+
});
1077+
}
1078+
}
11141079

11151080
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1081+
badInputChecker(scope, element, attr, ctrl);
11161082
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11171083

1084+
ctrl.$$parserName = 'number';
11181085
ctrl.$parsers.push(function(value) {
1119-
var empty = ctrl.$isEmpty(value);
1120-
if (empty || NUMBER_REGEXP.test(value)) {
1121-
ctrl.$setValidity('number', true);
1122-
return value === '' ? null : (empty ? value : parseFloat(value));
1123-
} else {
1124-
ctrl.$setValidity('number', false);
1125-
return undefined;
1126-
}
1086+
if(ctrl.$isEmpty(value)) return null;
1087+
if(NUMBER_REGEXP.test(value)) return parseFloat(value);
1088+
return undefined;
11271089
});
11281090

1129-
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
1130-
11311091
ctrl.$formatters.push(function(value) {
1132-
return ctrl.$isEmpty(value) ? '' : '' + value;
1092+
if (!ctrl.$isEmpty(value)) {
1093+
if (!isNumber(value)) {
1094+
throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
1095+
}
1096+
value = value.toString();
1097+
}
1098+
return value;
11331099
});
11341100

11351101
if (attr.min) {
1136-
var minValidator = function(value) {
1137-
var min = parseFloat(attr.min);
1138-
return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
1102+
ctrl.$validators.min = function(value) {
1103+
return ctrl.$isEmpty(value) || isUndefined(attr.min) || value >= parseFloat(attr.min);
11391104
};
1140-
1141-
ctrl.$parsers.push(minValidator);
1142-
ctrl.$formatters.push(minValidator);
11431105
}
11441106

11451107
if (attr.max) {
1146-
var maxValidator = function(value) {
1147-
var max = parseFloat(attr.max);
1148-
return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
1108+
ctrl.$validators.max = function(value) {
1109+
return ctrl.$isEmpty(value) || isUndefined(attr.max) || value <= parseFloat(attr.max);
11491110
};
1150-
1151-
ctrl.$parsers.push(maxValidator);
1152-
ctrl.$formatters.push(maxValidator);
11531111
}
1154-
1155-
ctrl.$formatters.push(function(value) {
1156-
return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
1157-
});
11581112
}
11591113

11601114
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1115+
badInputChecker(scope, element, attr, ctrl);
11611116
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11621117

1118+
ctrl.$$parserName = 'url';
11631119
ctrl.$validators.url = function(modelValue, viewValue) {
11641120
var value = modelValue || viewValue;
11651121
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
11661122
};
11671123
}
11681124

11691125
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1126+
badInputChecker(scope, element, attr, ctrl);
11701127
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
11711128

1129+
ctrl.$$parserName = 'email';
11721130
ctrl.$validators.email = function(modelValue, viewValue) {
11731131
var value = modelValue || viewValue;
11741132
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
@@ -1204,7 +1162,7 @@ function parseConstantExpr($parse, context, name, expression, fallback) {
12041162
if (isDefined(expression)) {
12051163
parseFn = $parse(expression);
12061164
if (!parseFn.constant) {
1207-
throw new minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
1165+
throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
12081166
'`{1}`.', name, expression);
12091167
}
12101168
return parseFn(context);
@@ -1579,7 +1537,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15791537
ctrl = this;
15801538

15811539
if (!ngModelSet) {
1582-
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
1540+
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
15831541
$attr.ngModel, startingTag($element));
15841542
}
15851543

@@ -1644,6 +1602,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16441602
$animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
16451603
}
16461604

1605+
this.$$clearValidity = function() {
1606+
forEach(ctrl.$error, function(val, key) {
1607+
var validationKey = snake_case(key, '-');
1608+
$animate.removeClass($element, VALID_CLASS + validationKey);
1609+
$animate.removeClass($element, INVALID_CLASS + validationKey);
1610+
});
1611+
1612+
invalidCount = 0;
1613+
$error = ctrl.$error = {};
1614+
1615+
parentForm.$$clearControlValidity(ctrl);
1616+
};
1617+
16471618
/**
16481619
* @ngdoc method
16491620
* @name ngModel.NgModelController#$setValidity
@@ -1675,7 +1646,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16751646
ctrl.$valid = true;
16761647
ctrl.$invalid = false;
16771648
}
1678-
} else {
1649+
} else if(!$error[validationErrorKey]) {
16791650
toggleValidCss(false);
16801651
ctrl.$invalid = true;
16811652
ctrl.$valid = false;
@@ -1864,16 +1835,27 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18641835
parentForm.$setDirty();
18651836
}
18661837

1867-
var modelValue = viewValue;
1868-
forEach(ctrl.$parsers, function(fn) {
1869-
modelValue = fn(modelValue);
1870-
});
1838+
var hasBadInput, modelValue = viewValue;
1839+
for(var i = 0; i < ctrl.$parsers.length; i++) {
1840+
modelValue = ctrl.$parsers[i](modelValue);
1841+
if(isUndefined(modelValue)) {
1842+
hasBadInput = true;
1843+
break;
1844+
}
1845+
}
18711846

1872-
if (ctrl.$modelValue !== modelValue &&
1873-
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
1847+
var parserName = ctrl.$$parserName || 'parse';
1848+
if (hasBadInput) {
1849+
ctrl.$$invalidModelValue = ctrl.$modelValue = undefined;
1850+
ctrl.$$clearValidity();
1851+
ctrl.$setValidity(parserName, false);
1852+
} else if (ctrl.$modelValue !== modelValue &&
1853+
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
1854+
ctrl.$setValidity(parserName, true);
18741855
ctrl.$$runValidators(modelValue, viewValue);
1875-
ctrl.$$writeModelToScope();
18761856
}
1857+
1858+
ctrl.$$writeModelToScope();
18771859
};
18781860

18791861
this.$$writeModelToScope = function() {

0 commit comments

Comments
 (0)