perf(forms): avoid direct references to the `Validators` class (#41189)

Currently the `Validators` class contains a number of static methods that represent different validators as well as some helper methods. Since class methods are not tree-shakable, any reference to the `Validator` class retains all of its methods (even if you've used just one).

This commit refactors the code to extract the logic into standalone functions and use these functions in the code instead of referencing them via `Validators` class. That should make the code more tree-shakable. The `Validators` class still retains its structure and calls these standalone methods internally to keep this change backwards-compatible.

PR Close #41189
This commit is contained in:
Andrew Kushnir 2021-03-11 19:42:20 -08:00 committed by Jessica Janiuk
parent 1644d64398
commit 3bd1992218
4 changed files with 202 additions and 119 deletions

View File

@ -728,6 +728,12 @@
{
"name": "collectStylingFromTAttrs"
},
{
"name": "compose"
},
{
"name": "composeAsync"
},
{
"name": "composeAsyncValidators"
},
@ -1343,6 +1349,9 @@
{
"name": "notFoundValueOrThrow"
},
{
"name": "nullValidator"
},
{
"name": "observable"
},

View File

@ -167,9 +167,6 @@
{
"name": "DomSharedStylesHost"
},
{
"name": "EMAIL_REGEXP"
},
{
"name": "EMPTY_ARRAY"
},
@ -563,9 +560,6 @@
{
"name": "VE_ViewContainerRef"
},
{
"name": "Validators"
},
{
"name": "Version"
},
@ -1046,9 +1040,6 @@
{
"name": "hasTagAndTypeMatch"
},
{
"name": "hasValidLength"
},
{
"name": "hostReportError"
},
@ -1130,9 +1121,6 @@
{
"name": "isDirectiveHost"
},
{
"name": "isEmptyInputValue"
},
{
"name": "isForwardRef"
},

View File

@ -10,7 +10,7 @@ import {Directive, forwardRef, Input, OnChanges, SimpleChanges, StaticProvider}
import {Observable} from 'rxjs';
import {AbstractControl} from '../model';
import {NG_VALIDATORS, Validators} from '../validators';
import {emailValidator, maxLengthValidator, maxValidator, minLengthValidator, minValidator, NG_VALIDATORS, nullValidator, patternValidator, requiredTrueValidator, requiredValidator} from '../validators';
/**
@ -77,7 +77,7 @@ export interface Validator {
*/
@Directive()
abstract class AbstractValidatorDirective implements Validator {
private _validator: ValidatorFn = Validators.nullValidator;
private _validator: ValidatorFn = nullValidator;
private _onChange!: () => void;
/**
@ -180,7 +180,7 @@ export class MaxValidator extends AbstractValidatorDirective implements OnChange
/** @internal */
normalizeInput = (input: string): number => parseInt(input, 10);
/** @internal */
createValidator = (max: number): ValidatorFn => Validators.max(max);
createValidator = (max: number): ValidatorFn => maxValidator(max);
/**
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
@ -240,7 +240,7 @@ export class MinValidator extends AbstractValidatorDirective implements OnChange
/** @internal */
normalizeInput = (input: string): number => parseInt(input, 10);
/** @internal */
createValidator = (min: number): ValidatorFn => Validators.min(min);
createValidator = (min: number): ValidatorFn => minValidator(min);
/**
* Declare `ngOnChanges` lifecycle hook at the main directive level (vs keeping it in base class)
* to avoid differences in handling inheritance of lifecycle hooks between Ivy and ViewEngine in
@ -364,7 +364,7 @@ export class RequiredValidator implements Validator {
* @nodoc
*/
validate(control: AbstractControl): ValidationErrors|null {
return this.required ? Validators.required(control) : null;
return this.required ? requiredValidator(control) : null;
}
/**
@ -411,7 +411,7 @@ export class CheckboxRequiredValidator extends RequiredValidator {
* @nodoc
*/
validate(control: AbstractControl): ValidationErrors|null {
return this.required ? Validators.requiredTrue(control) : null;
return this.required ? requiredTrueValidator(control) : null;
}
}
@ -472,7 +472,7 @@ export class EmailValidator implements Validator {
* @nodoc
*/
validate(control: AbstractControl): ValidationErrors|null {
return this._enabled ? Validators.email(control) : null;
return this._enabled ? emailValidator(control) : null;
}
/**
@ -543,7 +543,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
host: {'[attr.minlength]': 'minlength ? minlength : null'}
})
export class MinLengthValidator implements Validator, OnChanges {
private _validator: ValidatorFn = Validators.nullValidator;
private _validator: ValidatorFn = nullValidator;
private _onChange?: () => void;
/**
@ -579,7 +579,7 @@ export class MinLengthValidator implements Validator, OnChanges {
}
private _createValidator(): void {
this._validator = Validators.minLength(
this._validator = minLengthValidator(
typeof this.minlength === 'number' ? this.minlength : parseInt(this.minlength, 10));
}
}
@ -621,7 +621,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
host: {'[attr.maxlength]': 'maxlength ? maxlength : null'}
})
export class MaxLengthValidator implements Validator, OnChanges {
private _validator: ValidatorFn = Validators.nullValidator;
private _validator: ValidatorFn = nullValidator;
private _onChange?: () => void;
/**
@ -656,7 +656,7 @@ export class MaxLengthValidator implements Validator, OnChanges {
}
private _createValidator(): void {
this._validator = Validators.maxLength(
this._validator = maxLengthValidator(
typeof this.maxlength === 'number' ? this.maxlength : parseInt(this.maxlength, 10));
}
}
@ -701,7 +701,7 @@ export const PATTERN_VALIDATOR: any = {
host: {'[attr.pattern]': 'pattern ? pattern : null'}
})
export class PatternValidator implements Validator, OnChanges {
private _validator: ValidatorFn = Validators.nullValidator;
private _validator: ValidatorFn = nullValidator;
private _onChange?: () => void;
/**
@ -736,6 +736,6 @@ export class PatternValidator implements Validator, OnChanges {
}
private _createValidator(): void {
this._validator = Validators.pattern(this.pattern);
this._validator = patternValidator(this.pattern);
}
}

View File

@ -113,7 +113,6 @@ export class Validators {
/**
* @description
* Validator that requires the control's value to be greater than or equal to the provided number.
* The validator exists only as a function and not as a directive.
*
* @usageNotes
*
@ -132,21 +131,12 @@ export class Validators {
*
*/
static min(min: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// minimum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min
return !isNaN(value) && value < min ? {'min': {'min': min, 'actual': control.value}} : null;
};
return minValidator(min);
}
/**
* @description
* Validator that requires the control's value to be less than or equal to the provided number.
* The validator exists only as a function and not as a directive.
*
* @usageNotes
*
@ -165,15 +155,7 @@ export class Validators {
*
*/
static max(max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max
return !isNaN(value) && value > max ? {'max': {'max': max, 'actual': control.value}} : null;
};
return maxValidator(max);
}
/**
@ -197,7 +179,7 @@ export class Validators {
*
*/
static required(control: AbstractControl): ValidationErrors|null {
return isEmptyInputValue(control.value) ? {'required': true} : null;
return requiredValidator(control);
}
/**
@ -222,7 +204,7 @@ export class Validators {
*
*/
static requiredTrue(control: AbstractControl): ValidationErrors|null {
return control.value === true ? null : {'required': true};
return requiredTrueValidator(control);
}
/**
@ -262,10 +244,7 @@ export class Validators {
*
*/
static email(control: AbstractControl): ValidationErrors|null {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
return EMAIL_REGEXP.test(control.value) ? null : {'email': true};
return emailValidator(control);
}
/**
@ -299,17 +278,7 @@ export class Validators {
*
*/
static minLength(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || !hasValidLength(control.value)) {
// don't validate empty values to allow optional controls
// don't validate values without `length` property
return null;
}
return control.value.length < minLength ?
{'minlength': {'requiredLength': minLength, 'actualLength': control.value.length}} :
null;
};
return minLengthValidator(minLength);
}
/**
@ -340,11 +309,7 @@ export class Validators {
*
*/
static maxLength(maxLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
return hasValidLength(control.value) && control.value.length > maxLength ?
{'maxlength': {'requiredLength': maxLength, 'actualLength': control.value.length}} :
null;
};
return maxLengthValidator(maxLength);
}
/**
@ -397,7 +362,149 @@ export class Validators {
*
*/
static pattern(pattern: string|RegExp): ValidatorFn {
if (!pattern) return Validators.nullValidator;
return patternValidator(pattern);
}
/**
* @description
* Validator that performs no operation.
*
* @see `updateValueAndValidity()`
*
*/
static nullValidator(control: AbstractControl): ValidationErrors|null {
return nullValidator(control);
}
/**
* @description
* Compose multiple validators into a single function that returns the union
* of the individual error maps for the provided control.
*
* @returns A validator function that returns an error map with the
* merged error maps of the validators if the validation check fails, otherwise `null`.
*
* @see `updateValueAndValidity()`
*
*/
static compose(validators: null): null;
static compose(validators: (ValidatorFn|null|undefined)[]): ValidatorFn|null;
static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
return compose(validators);
}
/**
* @description
* Compose multiple async validators into a single function that returns the union
* of the individual error objects for the provided control.
*
* @returns A validator function that returns an error map with the
* merged error objects of the async validators if the validation check fails, otherwise `null`.
*
* @see `updateValueAndValidity()`
*
*/
static composeAsync(validators: (AsyncValidatorFn|null)[]): AsyncValidatorFn|null {
return composeAsync(validators);
}
}
/**
* Validator that requires the control's value to be greater than or equal to the provided number.
* See `Validators.min` for additional information.
*/
export function minValidator(min: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// minimum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min
return !isNaN(value) && value < min ? {'min': {'min': min, 'actual': control.value}} : null;
};
}
/**
* Validator that requires the control's value to be less than or equal to the provided number.
* See `Validators.max` for additional information.
*/
export function maxValidator(max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max
return !isNaN(value) && value > max ? {'max': {'max': max, 'actual': control.value}} : null;
};
}
/**
* Validator that requires the control have a non-empty value.
* See `Validators.required` for additional information.
*/
export function requiredValidator(control: AbstractControl): ValidationErrors|null {
return isEmptyInputValue(control.value) ? {'required': true} : null;
}
/**
* Validator that requires the control's value be true. This validator is commonly
* used for required checkboxes.
* See `Validators.requiredTrue` for additional information.
*/
export function requiredTrueValidator(control: AbstractControl): ValidationErrors|null {
return control.value === true ? null : {'required': true};
}
/**
* Validator that requires the control's value pass an email validation test.
* See `Validators.email` for additional information.
*/
export function emailValidator(control: AbstractControl): ValidationErrors|null {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
return EMAIL_REGEXP.test(control.value) ? null : {'email': true};
}
/**
* Validator that requires the length of the control's value to be greater than or equal
* to the provided minimum length. See `Validators.minLength` for additional information.
*/
export function minLengthValidator(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
if (isEmptyInputValue(control.value) || !hasValidLength(control.value)) {
// don't validate empty values to allow optional controls
// don't validate values without `length` property
return null;
}
return control.value.length < minLength ?
{'minlength': {'requiredLength': minLength, 'actualLength': control.value.length}} :
null;
};
}
/**
* Validator that requires the length of the control's value to be less than or equal
* to the provided maximum length. See `Validators.maxLength` for additional information.
*/
export function maxLengthValidator(maxLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors|null => {
return hasValidLength(control.value) && control.value.length > maxLength ?
{'maxlength': {'requiredLength': maxLength, 'actualLength': control.value.length}} :
null;
};
}
/**
* Validator that requires the control's value to match a regex pattern.
* See `Validators.pattern` for additional information.
*/
export function patternValidator(pattern: string|RegExp): ValidatorFn {
if (!pattern) return nullValidator;
let regex: RegExp;
let regexStr: string;
if (typeof pattern === 'string') {
@ -422,64 +529,13 @@ export class Validators {
return regex.test(value) ? null :
{'pattern': {'requiredPattern': regexStr, 'actualValue': value}};
};
}
}
/**
* @description
* Validator that performs no operation.
*
* @see `updateValueAndValidity()`
*
/**
* Function that has `ValidatorFn` shape, but performs no operation.
*/
static nullValidator(control: AbstractControl): ValidationErrors|null {
export function nullValidator(control: AbstractControl): ValidationErrors|null {
return null;
}
/**
* @description
* Compose multiple validators into a single function that returns the union
* of the individual error maps for the provided control.
*
* @returns A validator function that returns an error map with the
* merged error maps of the validators if the validation check fails, otherwise `null`.
*
* @see `updateValueAndValidity()`
*
*/
static compose(validators: null): null;
static compose(validators: (ValidatorFn|null|undefined)[]): ValidatorFn|null;
static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
if (!validators) return null;
const presentValidators: ValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
return mergeErrors(executeValidators<ValidatorFn>(control, presentValidators));
};
}
/**
* @description
* Compose multiple async validators into a single function that returns the union
* of the individual error objects for the provided control.
*
* @returns A validator function that returns an error map with the
* merged error objects of the async validators if the validation check fails, otherwise `null`.
*
* @see `updateValueAndValidity()`
*
*/
static composeAsync(validators: (AsyncValidatorFn|null)[]): AsyncValidatorFn|null {
if (!validators) return null;
const presentValidators: AsyncValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
const observables =
executeValidators<AsyncValidatorFn>(control, presentValidators).map(toObservable);
return forkJoin(observables).pipe(map(mergeErrors));
};
}
}
function isPresent(o: any): boolean {
@ -534,22 +590,52 @@ export function normalizeValidators<V>(validators: (V|Validator|AsyncValidator)[
}
/**
* Merges synchronous validators into a single validator function (combined using
* `Validators.compose`).
* Merges synchronous validators into a single validator function.
* See `Validators.compose` for additional information.
*/
export function composeValidators(validators: Array<Validator|ValidatorFn>): ValidatorFn|null {
return validators != null ? Validators.compose(normalizeValidators<ValidatorFn>(validators)) :
null;
function compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
if (!validators) return null;
const presentValidators: ValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
return mergeErrors(executeValidators<ValidatorFn>(control, presentValidators));
};
}
/**
* Merges asynchronous validators into a single validator function (combined using
* `Validators.composeAsync`).
* Accepts a list of validators of different possible shapes (`Validator` and `ValidatorFn`),
* normalizes the list (converts everything to `ValidatorFn`) and merges them into a single
* validator function.
*/
export function composeValidators(validators: Array<Validator|ValidatorFn>): ValidatorFn|null {
return validators != null ? compose(normalizeValidators<ValidatorFn>(validators)) : null;
}
/**
* Merges asynchronous validators into a single validator function.
* See `Validators.composeAsync` for additional information.
*/
function composeAsync(validators: (AsyncValidatorFn|null)[]): AsyncValidatorFn|null {
if (!validators) return null;
const presentValidators: AsyncValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
const observables =
executeValidators<AsyncValidatorFn>(control, presentValidators).map(toObservable);
return forkJoin(observables).pipe(map(mergeErrors));
};
}
/**
* Accepts a list of async validators of different possible shapes (`AsyncValidator` and
* `AsyncValidatorFn`), normalizes the list (converts everything to `AsyncValidatorFn`) and merges
* them into a single validator function.
*/
export function composeAsyncValidators(validators: Array<AsyncValidator|AsyncValidatorFn>):
AsyncValidatorFn|null {
return validators != null ?
Validators.composeAsync(normalizeValidators<AsyncValidatorFn>(validators)) :
return validators != null ? composeAsync(normalizeValidators<AsyncValidatorFn>(validators)) :
null;
}