perf(input): prevent multiple validations on initialization
This commit updates in-built validators with observers to prevent multiple calls to $validate that could happen on initial linking of the directives in certain circumstances: - when an input is wrapped in a transclude: element directive (e.g. ngRepeat), the order of execution between ngModel and the input / validation directives changes so that the initial observer call happens when ngModel has already been initalized, leading to another call to $validate, which calls *all* defined validators again. Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators. When using validators with scope expressions, the expression value is not available when ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to the observer does the value become available, making a call to $validate a necessity. This commit solves the first problem by storing the validation attribute value so we can compare the current value and the observed value - which will be the same after compilation. The second problem is solved by parsing the validation expression once in the link function, so the value is available when ngModel first validates. Closes #14691 Closes #16760
This commit is contained in:
+69
-29
@@ -1497,7 +1497,7 @@ function createDateParser(regexp, mapping) {
|
||||
}
|
||||
|
||||
function createDateInputType(type, regexp, parseDate, format) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
||||
badInputChecker(scope, element, attr, ctrl, type);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
@@ -1540,24 +1540,34 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
});
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
var minVal;
|
||||
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
||||
var parsedMinVal = parseObservedDateValue(minVal);
|
||||
|
||||
ctrl.$validators.min = function(value) {
|
||||
return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
|
||||
return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
|
||||
};
|
||||
attr.$observe('min', function(val) {
|
||||
minVal = parseObservedDateValue(val);
|
||||
ctrl.$validate();
|
||||
if (val !== minVal) {
|
||||
parsedMinVal = parseObservedDateValue(val);
|
||||
minVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
var maxVal;
|
||||
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
||||
var parsedMaxVal = parseObservedDateValue(maxVal);
|
||||
|
||||
ctrl.$validators.max = function(value) {
|
||||
return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
|
||||
return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
|
||||
};
|
||||
attr.$observe('max', function(val) {
|
||||
maxVal = parseObservedDateValue(val);
|
||||
ctrl.$validate();
|
||||
if (val !== maxVal) {
|
||||
parsedMaxVal = parseObservedDateValue(val);
|
||||
maxVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1709,50 +1719,68 @@ function isValidForStep(viewValue, stepBase, step) {
|
||||
return (value - stepBase) % step === 0;
|
||||
}
|
||||
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
||||
badInputChecker(scope, element, attr, ctrl, 'number');
|
||||
numberFormatterParser(ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
var minVal;
|
||||
var maxVal;
|
||||
var parsedMinVal;
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
||||
parsedMinVal = parseNumberAttrVal(minVal);
|
||||
|
||||
ctrl.$validators.min = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
|
||||
};
|
||||
|
||||
attr.$observe('min', function(val) {
|
||||
minVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== minVal) {
|
||||
parsedMinVal = parseNumberAttrVal(val);
|
||||
minVal = val;
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
||||
var parsedMaxVal = parseNumberAttrVal(maxVal);
|
||||
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
|
||||
};
|
||||
|
||||
attr.$observe('max', function(val) {
|
||||
maxVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== maxVal) {
|
||||
parsedMaxVal = parseNumberAttrVal(val);
|
||||
maxVal = val;
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.step) || attr.ngStep) {
|
||||
var stepVal;
|
||||
var stepVal = attr.step || $parse(attr.ngStep)(scope);
|
||||
var parsedStepVal = parseNumberAttrVal(stepVal);
|
||||
|
||||
ctrl.$validators.step = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
|
||||
isValidForStep(viewValue, minVal || 0, stepVal);
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
|
||||
isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
|
||||
};
|
||||
|
||||
attr.$observe('step', function(val) {
|
||||
stepVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== stepVal) {
|
||||
parsedStepVal = parseNumberAttrVal(val);
|
||||
stepVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,6 +1810,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
originalRender;
|
||||
|
||||
if (hasMinAttr) {
|
||||
minVal = parseNumberAttrVal(attr.min);
|
||||
|
||||
ctrl.$validators.min = supportsRange ?
|
||||
// Since all browsers set the input to a valid value, we don't need to check validity
|
||||
function noopMinValidator() { return true; } :
|
||||
@@ -1794,6 +1824,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (hasMaxAttr) {
|
||||
maxVal = parseNumberAttrVal(attr.max);
|
||||
|
||||
ctrl.$validators.max = supportsRange ?
|
||||
// Since all browsers set the input to a valid value, we don't need to check validity
|
||||
function noopMaxValidator() { return true; } :
|
||||
@@ -1806,6 +1838,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (hasStepAttr) {
|
||||
stepVal = parseNumberAttrVal(attr.step);
|
||||
|
||||
ctrl.$validators.step = supportsRange ?
|
||||
function nativeStepValidator() {
|
||||
// Currently, only FF implements the spec on step change correctly (i.e. adjusting the
|
||||
@@ -1827,7 +1861,13 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
// attribute value when the input is first rendered, so that the browser can adjust the
|
||||
// input value based on the min/max value
|
||||
element.attr(htmlAttrName, attr[htmlAttrName]);
|
||||
attr.$observe(htmlAttrName, changeFn);
|
||||
var oldVal = attr[htmlAttrName];
|
||||
attr.$observe(htmlAttrName, function wrappedObserver(val) {
|
||||
if (val !== oldVal) {
|
||||
oldVal = val;
|
||||
changeFn(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function minChange(val) {
|
||||
@@ -1881,11 +1921,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
|
||||
if (supportsRange && ctrl.$viewValue !== element.val()) {
|
||||
ctrl.$setViewValue(element.val());
|
||||
} else {
|
||||
if (!supportsRange) {
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
} else if (ctrl.$viewValue !== element.val()) {
|
||||
ctrl.$setViewValue(element.val());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@ NgModelController.prototype = {
|
||||
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
|
||||
*/
|
||||
$validate: function() {
|
||||
|
||||
// ignore $validate before model is initialized
|
||||
if (isNumberNaN(this.$modelValue)) {
|
||||
return;
|
||||
|
||||
@@ -62,24 +62,29 @@
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var requiredDirective = function() {
|
||||
var requiredDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
var oldVal = attr.required || $parse(attr.ngRequired)(scope);
|
||||
|
||||
attr.required = true; // force truthy in case we are on non input element
|
||||
|
||||
ctrl.$validators.required = function(modelValue, viewValue) {
|
||||
return !attr.required || !ctrl.$isEmpty(viewValue);
|
||||
};
|
||||
|
||||
attr.$observe('required', function() {
|
||||
ctrl.$validate();
|
||||
attr.$observe('required', function(val) {
|
||||
if (oldVal !== val) {
|
||||
oldVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -162,36 +167,59 @@ var requiredDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var patternDirective = function() {
|
||||
var patternDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
compile: function(tElm, tAttr) {
|
||||
var patternExp;
|
||||
var parseFn;
|
||||
|
||||
var regexp, patternExp = attr.ngPattern || attr.pattern;
|
||||
attr.$observe('pattern', function(regex) {
|
||||
if (isString(regex) && regex.length > 0) {
|
||||
regex = new RegExp('^' + regex + '$');
|
||||
if (tAttr.ngPattern) {
|
||||
patternExp = tAttr.ngPattern;
|
||||
|
||||
// ngPattern might be a scope expression, or an inlined regex, which is not parsable.
|
||||
// We get value of the attribute here, so we can compare the old and the new value
|
||||
// in the observer to avoid unnecessary validations
|
||||
if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
|
||||
parseFn = function() { return tAttr.ngPattern; };
|
||||
} else {
|
||||
parseFn = $parse(tAttr.ngPattern);
|
||||
}
|
||||
}
|
||||
|
||||
return function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var attrVal = attr.pattern;
|
||||
|
||||
if (attr.ngPattern) {
|
||||
attrVal = parseFn(scope);
|
||||
} else {
|
||||
patternExp = attr.pattern;
|
||||
}
|
||||
|
||||
if (regex && !regex.test) {
|
||||
throw minErr('ngPattern')('noregexp',
|
||||
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
|
||||
regex, startingTag(elm));
|
||||
}
|
||||
var regexp = parsePatternAttr(attrVal, patternExp, elm);
|
||||
|
||||
regexp = regex || undefined;
|
||||
ctrl.$validate();
|
||||
});
|
||||
attr.$observe('pattern', function(newVal) {
|
||||
var oldRegexp = regexp;
|
||||
|
||||
ctrl.$validators.pattern = function(modelValue, viewValue) {
|
||||
// HTML5 pattern constraint validates the input value, so we validate the viewValue
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
|
||||
regexp = parsePatternAttr(newVal, patternExp, elm);
|
||||
|
||||
if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.$validators.pattern = function(modelValue, viewValue) {
|
||||
// HTML5 pattern constraint validates the input value, so we validate the viewValue
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -264,25 +292,29 @@ var patternDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var maxlengthDirective = function() {
|
||||
var maxlengthDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var maxlength = -1;
|
||||
var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
|
||||
var maxlengthParsed = parseLength(maxlength);
|
||||
|
||||
attr.$observe('maxlength', function(value) {
|
||||
var intVal = toInt(value);
|
||||
maxlength = isNumberNaN(intVal) ? -1 : intVal;
|
||||
ctrl.$validate();
|
||||
if (maxlength !== value) {
|
||||
maxlengthParsed = parseLength(value);
|
||||
maxlength = value;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
ctrl.$validators.maxlength = function(modelValue, viewValue) {
|
||||
return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
|
||||
return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -353,21 +385,49 @@ var maxlengthDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var minlengthDirective = function() {
|
||||
var minlengthDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var minlength = 0;
|
||||
var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
|
||||
var minlengthParsed = parseLength(minlength) || -1;
|
||||
|
||||
attr.$observe('minlength', function(value) {
|
||||
minlength = toInt(value) || 0;
|
||||
ctrl.$validate();
|
||||
if (minlength !== value) {
|
||||
minlengthParsed = parseLength(value) || -1;
|
||||
minlength = value;
|
||||
ctrl.$validate();
|
||||
}
|
||||
|
||||
});
|
||||
ctrl.$validators.minlength = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
|
||||
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
|
||||
function parsePatternAttr(regex, patternExp, elm) {
|
||||
if (!regex) return undefined;
|
||||
|
||||
if (isString(regex)) {
|
||||
regex = new RegExp('^' + regex + '$');
|
||||
}
|
||||
|
||||
if (!regex.test) {
|
||||
throw minErr('ngPattern')('noregexp',
|
||||
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
|
||||
regex, startingTag(elm));
|
||||
}
|
||||
|
||||
return regex;
|
||||
}
|
||||
|
||||
function parseLength(val) {
|
||||
var intVal = toInt(val);
|
||||
return isNumberNaN(intVal) ? -1 : intVal;
|
||||
}
|
||||
|
||||
@@ -312,7 +312,28 @@ window.dump = function() {
|
||||
|
||||
function generateInputCompilerHelper(helper) {
|
||||
beforeEach(function() {
|
||||
helper.validationCounter = {};
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('validationSpy', function() {
|
||||
return {
|
||||
priority: 1,
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var validationName = attrs.validationSpy;
|
||||
|
||||
var originalValidator = ctrl.$validators[validationName];
|
||||
helper.validationCounter[validationName] = 0;
|
||||
|
||||
ctrl.$validators[validationName] = function(modelValue, viewValue) {
|
||||
helper.validationCounter[validationName]++;
|
||||
|
||||
return originalValidator(modelValue, viewValue);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$compileProvider.directive('attrCapture', function() {
|
||||
return function(scope, element, $attrs) {
|
||||
helper.attrs = $attrs;
|
||||
|
||||
@@ -839,6 +839,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -898,6 +914,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1114,6 +1146,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -1176,6 +1224,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1506,6 +1570,23 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -1565,6 +1646,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1972,6 +2069,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -2019,6 +2132,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2361,6 +2490,26 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = '2000-01-01';
|
||||
$rootScope.value = new Date(2010, 1, 1, 0, 0, 0);
|
||||
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -2428,6 +2577,25 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxVal = '2000-01-01';
|
||||
$rootScope.value = new Date(2020, 1, 1, 0, 0, 0);
|
||||
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3063,6 +3231,18 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.minVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ngMin', function() {
|
||||
@@ -3131,6 +3311,17 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.minVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3200,6 +3391,18 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.maxVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="max" name="alias" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ngMax', function() {
|
||||
@@ -3268,6 +3471,17 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.maxVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3364,7 +3578,7 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.value).toBe(10); // an initially invalid value should not be changed
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
@@ -3444,6 +3658,17 @@ describe('input', function() {
|
||||
expect($rootScope.value).toBe(1.16);
|
||||
}
|
||||
);
|
||||
|
||||
it('should validate only once after compilation inside ngRepeat', function() {
|
||||
$rootScope.step = 10;
|
||||
$rootScope.value = 20;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" name="alias" ' + attrHtml + ' validation-spy="step" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.step).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3485,6 +3710,16 @@ describe('input', function() {
|
||||
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 'text';
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input ng-model="value" validation-spy="required" required />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngRequired', function() {
|
||||
@@ -3534,6 +3769,17 @@ describe('input', function() {
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.form.numberInput.$error.required).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 'text';
|
||||
$rootScope.isRequired = true;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input ng-model="value" validation-spy="required" ng-required="isRequired" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the ngRequired expression initially evaluates to false', function() {
|
||||
@@ -3848,6 +4094,17 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('20');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
// input[type=range] will become type=text in browsers that don't support it
|
||||
|
||||
@@ -3926,6 +4183,16 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('15');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4006,6 +4273,17 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('0');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat and the value is valid', function() {
|
||||
$rootScope.maxVal = 5;
|
||||
$rootScope.value = 5;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
it('should validate if "range" is not implemented', function() {
|
||||
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
|
||||
@@ -4081,6 +4359,17 @@ describe('input', function() {
|
||||
expect(scope.value).toBe(5);
|
||||
expect(inputElm.val()).toBe('5');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4183,6 +4472,18 @@ describe('input', function() {
|
||||
expect(scope.value).toBe(10);
|
||||
expect(scope.form.alias.$error.step).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.stepVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="step" step="{{ stepVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.step).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
it('should validate if "range" is not implemented', function() {
|
||||
@@ -4269,7 +4570,7 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.value).toBe(10);
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
|
||||
@@ -863,7 +863,6 @@ describe('ngModel', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('view -> model update', function() {
|
||||
|
||||
it('should always perform validations using the parsed model value', function() {
|
||||
|
||||
@@ -230,6 +230,29 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.pattern).toBe(true);
|
||||
expect(ctrlNg.$error.pattern).toBe(true);
|
||||
}));
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
|
||||
$rootScope.pattern = /\d{4}/;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" pattern="\\d{4}" validation-spy="pattern" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.pattern).toBe(1);
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-pattern="pattern" validation-spy="pattern" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.pattern).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -312,9 +335,31 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.minlength).toBe(true);
|
||||
expect(ctrlNg.$error.minlength).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minlength = 5;
|
||||
|
||||
var element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" minlength="{{minlength}}" validation-spy="minlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.minlength).toBe(1);
|
||||
|
||||
element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-minlength="minlength" validation-spy="minlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.minlength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxlength', function() {
|
||||
|
||||
it('should invalidate values that are longer than the given maxlength', function() {
|
||||
@@ -500,6 +545,29 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.maxlength).toBe(true);
|
||||
expect(ctrlNg.$error.maxlength).toBe(true);
|
||||
}));
|
||||
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxlength = 5;
|
||||
|
||||
var element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" maxlength="{{maxlength}}" validation-spy="maxlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.maxlength).toBe(1);
|
||||
|
||||
element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-maxlength="maxlength" validation-spy="maxlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.maxlength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -626,5 +694,41 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.required).toBe(true);
|
||||
expect(ctrlNg.$error.required).toBe(true);
|
||||
}));
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat', function() {
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" required validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat and ngRequired is true', function() {
|
||||
$rootScope.isRequired = true;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat and ngRequired is false', function() {
|
||||
$rootScope.isRequired = false;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user