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

Commit 0637a21

Browse files
authored
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
1 parent d855b74 commit 0637a21

File tree

7 files changed

+594
-68
lines changed

7 files changed

+594
-68
lines changed

src/ng/directive/input.js

+69-29
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ function createDateParser(regexp, mapping) {
14971497
}
14981498

14991499
function createDateInputType(type, regexp, parseDate, format) {
1500-
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
1500+
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
15011501
badInputChecker(scope, element, attr, ctrl, type);
15021502
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
15031503

@@ -1540,24 +1540,34 @@ function createDateInputType(type, regexp, parseDate, format) {
15401540
});
15411541

15421542
if (isDefined(attr.min) || attr.ngMin) {
1543-
var minVal;
1543+
var minVal = attr.min || $parse(attr.ngMin)(scope);
1544+
var parsedMinVal = parseObservedDateValue(minVal);
1545+
15441546
ctrl.$validators.min = function(value) {
1545-
return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
1547+
return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
15461548
};
15471549
attr.$observe('min', function(val) {
1548-
minVal = parseObservedDateValue(val);
1549-
ctrl.$validate();
1550+
if (val !== minVal) {
1551+
parsedMinVal = parseObservedDateValue(val);
1552+
minVal = val;
1553+
ctrl.$validate();
1554+
}
15501555
});
15511556
}
15521557

15531558
if (isDefined(attr.max) || attr.ngMax) {
1554-
var maxVal;
1559+
var maxVal = attr.max || $parse(attr.ngMax)(scope);
1560+
var parsedMaxVal = parseObservedDateValue(maxVal);
1561+
15551562
ctrl.$validators.max = function(value) {
1556-
return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
1563+
return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
15571564
};
15581565
attr.$observe('max', function(val) {
1559-
maxVal = parseObservedDateValue(val);
1560-
ctrl.$validate();
1566+
if (val !== maxVal) {
1567+
parsedMaxVal = parseObservedDateValue(val);
1568+
maxVal = val;
1569+
ctrl.$validate();
1570+
}
15611571
});
15621572
}
15631573

@@ -1709,50 +1719,68 @@ function isValidForStep(viewValue, stepBase, step) {
17091719
return (value - stepBase) % step === 0;
17101720
}
17111721

1712-
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1722+
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
17131723
badInputChecker(scope, element, attr, ctrl, 'number');
17141724
numberFormatterParser(ctrl);
17151725
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
17161726

1717-
var minVal;
1718-
var maxVal;
1727+
var parsedMinVal;
17191728

17201729
if (isDefined(attr.min) || attr.ngMin) {
1730+
var minVal = attr.min || $parse(attr.ngMin)(scope);
1731+
parsedMinVal = parseNumberAttrVal(minVal);
1732+
17211733
ctrl.$validators.min = function(modelValue, viewValue) {
1722-
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
1734+
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
17231735
};
17241736

17251737
attr.$observe('min', function(val) {
1726-
minVal = parseNumberAttrVal(val);
1727-
// TODO(matsko): implement validateLater to reduce number of validations
1728-
ctrl.$validate();
1738+
if (val !== minVal) {
1739+
parsedMinVal = parseNumberAttrVal(val);
1740+
minVal = val;
1741+
// TODO(matsko): implement validateLater to reduce number of validations
1742+
ctrl.$validate();
1743+
}
17291744
});
17301745
}
17311746

17321747
if (isDefined(attr.max) || attr.ngMax) {
1748+
var maxVal = attr.max || $parse(attr.ngMax)(scope);
1749+
var parsedMaxVal = parseNumberAttrVal(maxVal);
1750+
17331751
ctrl.$validators.max = function(modelValue, viewValue) {
1734-
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
1752+
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
17351753
};
17361754

17371755
attr.$observe('max', function(val) {
1738-
maxVal = parseNumberAttrVal(val);
1739-
// TODO(matsko): implement validateLater to reduce number of validations
1740-
ctrl.$validate();
1756+
if (val !== maxVal) {
1757+
parsedMaxVal = parseNumberAttrVal(val);
1758+
maxVal = val;
1759+
// TODO(matsko): implement validateLater to reduce number of validations
1760+
ctrl.$validate();
1761+
}
17411762
});
17421763
}
17431764

17441765
if (isDefined(attr.step) || attr.ngStep) {
1745-
var stepVal;
1766+
var stepVal = attr.step || $parse(attr.ngStep)(scope);
1767+
var parsedStepVal = parseNumberAttrVal(stepVal);
1768+
17461769
ctrl.$validators.step = function(modelValue, viewValue) {
1747-
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
1748-
isValidForStep(viewValue, minVal || 0, stepVal);
1770+
return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
1771+
isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
17491772
};
17501773

17511774
attr.$observe('step', function(val) {
1752-
stepVal = parseNumberAttrVal(val);
17531775
// TODO(matsko): implement validateLater to reduce number of validations
1754-
ctrl.$validate();
1776+
if (val !== stepVal) {
1777+
parsedStepVal = parseNumberAttrVal(val);
1778+
stepVal = val;
1779+
ctrl.$validate();
1780+
}
1781+
17551782
});
1783+
17561784
}
17571785
}
17581786

@@ -1782,6 +1810,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
17821810
originalRender;
17831811

17841812
if (hasMinAttr) {
1813+
minVal = parseNumberAttrVal(attr.min);
1814+
17851815
ctrl.$validators.min = supportsRange ?
17861816
// Since all browsers set the input to a valid value, we don't need to check validity
17871817
function noopMinValidator() { return true; } :
@@ -1794,6 +1824,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
17941824
}
17951825

17961826
if (hasMaxAttr) {
1827+
maxVal = parseNumberAttrVal(attr.max);
1828+
17971829
ctrl.$validators.max = supportsRange ?
17981830
// Since all browsers set the input to a valid value, we don't need to check validity
17991831
function noopMaxValidator() { return true; } :
@@ -1806,6 +1838,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
18061838
}
18071839

18081840
if (hasStepAttr) {
1841+
stepVal = parseNumberAttrVal(attr.step);
1842+
18091843
ctrl.$validators.step = supportsRange ?
18101844
function nativeStepValidator() {
18111845
// 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) {
18271861
// attribute value when the input is first rendered, so that the browser can adjust the
18281862
// input value based on the min/max value
18291863
element.attr(htmlAttrName, attr[htmlAttrName]);
1830-
attr.$observe(htmlAttrName, changeFn);
1864+
var oldVal = attr[htmlAttrName];
1865+
attr.$observe(htmlAttrName, function wrappedObserver(val) {
1866+
if (val !== oldVal) {
1867+
oldVal = val;
1868+
changeFn(val);
1869+
}
1870+
});
18311871
}
18321872

18331873
function minChange(val) {
@@ -1881,11 +1921,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
18811921
}
18821922

18831923
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
1884-
if (supportsRange && ctrl.$viewValue !== element.val()) {
1885-
ctrl.$setViewValue(element.val());
1886-
} else {
1924+
if (!supportsRange) {
18871925
// TODO(matsko): implement validateLater to reduce number of validations
18881926
ctrl.$validate();
1927+
} else if (ctrl.$viewValue !== element.val()) {
1928+
ctrl.$setViewValue(element.val());
18891929
}
18901930
}
18911931
}

src/ng/directive/ngModel.js

+1
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ NgModelController.prototype = {
562562
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
563563
*/
564564
$validate: function() {
565+
565566
// ignore $validate before model is initialized
566567
if (isNumberNaN(this.$modelValue)) {
567568
return;

0 commit comments

Comments
 (0)