feat(forms): add hasValidators, addValidators, and removeValidators methods (for both sync and async) (#42838)

Several new functionalities are possible with this change: the most requested is that callers can now check whether a control has a required validator. Other uses include incrementally changing the validators set without doing an expensive operation to reset all validators.

Closes #13461.

PR Close #42838
This commit is contained in:
Dylan Hunn
2021-07-12 10:41:02 -07:00
parent 7e9673d14a
commit 1d9d02696e
8 changed files with 309 additions and 19 deletions
+106 -16
View File
@@ -11,7 +11,7 @@ import {Observable} from 'rxjs';
import {removeListItem} from './directives/shared';
import {AsyncValidatorFn, ValidationErrors, ValidatorFn} from './directives/validators';
import {composeAsyncValidators, composeValidators, toObservable} from './validators';
import {addValidators, composeAsyncValidators, composeValidators, hasValidator, makeValidatorsArray, removeValidators, toObservable} from './validators';
/**
* Reports that a FormControl is valid, meaning that no errors exist in the input value.
@@ -129,14 +129,12 @@ export interface AbstractControlOptions {
updateOn?: 'change'|'blur'|'submit';
}
function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|
null): validatorOrOpts is AbstractControlOptions {
return validatorOrOpts != null && !Array.isArray(validatorOrOpts) &&
typeof validatorOrOpts === 'object';
}
/**
* This is the base class for `FormControl`, `FormGroup`, and `FormArray`.
*
@@ -245,7 +243,9 @@ export abstract class AbstractControl {
}
/**
* The function that is used to determine the validity of this control synchronously.
* Returns the function that is used to determine the validity of this control synchronously.
* If multiple validators have been added, this will be a single composed function.
* See `Validators.compose()` for additional information.
*/
get validator(): ValidatorFn|null {
return this._composedValidatorFn;
@@ -255,7 +255,9 @@ export abstract class AbstractControl {
}
/**
* The function that is used to determine the validity of this control asynchronously.
* Returns the function that is used to determine the validity of this control asynchronously.
* If multiple validators have been added, this will be a single composed function.
* See `Validators.compose()` for additional information.
*/
get asyncValidator(): AsyncValidatorFn|null {
return this._composedAsyncValidatorFn;
@@ -425,32 +427,120 @@ export abstract class AbstractControl {
/**
* Sets the synchronous validators that are active on this control. Calling
* this overwrites any existing sync validators.
* this overwrites any existing synchronous validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* If you want to add a new validator without affecting existing ones, consider
* using `addValidators()` method instead.
*/
setValidators(newValidator: ValidatorFn|ValidatorFn[]|null): void {
this._rawValidators = newValidator;
this._composedValidatorFn = coerceToValidator(newValidator);
setValidators(validators: ValidatorFn|ValidatorFn[]|null): void {
this._rawValidators = validators;
this._composedValidatorFn = coerceToValidator(validators);
}
/**
* Sets the async validators that are active on this control. Calling this
* overwrites any existing async validators.
* Sets the asynchronous validators that are active on this control. Calling this
* overwrites any existing asynchronous validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* If you want to add a new validator without affecting existing ones, consider
* using `addAsyncValidators()` method instead.
*/
setAsyncValidators(newValidator: AsyncValidatorFn|AsyncValidatorFn[]|null): void {
this._rawAsyncValidators = newValidator;
this._composedAsyncValidatorFn = coerceToAsyncValidator(newValidator);
setAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]|null): void {
this._rawAsyncValidators = validators;
this._composedAsyncValidatorFn = coerceToAsyncValidator(validators);
}
/**
* Empties out the sync validator list.
* Add a synchronous validator or validators to this control, without affecting other validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* Adding a validator that already exists will have no effect. If duplicate validator functions
* are present in the `validators` array, only the first instance would be added to a form
* control.
*
* @param validators The new validator function or functions to add to this control.
*/
addValidators(validators: ValidatorFn|ValidatorFn[]): void {
this.setValidators(addValidators(validators, this._rawValidators));
}
/**
* Add an asynchronous validator or validators to this control, without affecting other
* validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* Adding a validator that already exists will have no effect.
*
* @param validators The new asynchronous validator function or functions to add to this control.
*/
addAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void {
this.setAsyncValidators(addValidators(validators, this._rawAsyncValidators));
}
/**
* Remove a synchronous validator from this control, without affecting other validators.
* Validators are compared by function reference; you must pass a reference to the exact same
* validator function as the one that was originally set. If a provided validator is not found,
* it is ignored.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* @param validators The validator or validators to remove.
*/
removeValidators(validators: ValidatorFn|ValidatorFn[]): void {
this.setValidators(removeValidators(validators, this._rawValidators));
}
/**
* Remove an asynchronous validator from this control, without affecting other validators.
* Validators are compared by function reference; you must pass a reference to the exact same
* validator function as the one that was originally set. If a provided validator is not found, it
* is ignored.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
* @param validators The asynchronous validator or validators to remove.
*/
removeAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void {
this.setAsyncValidators(removeValidators(validators, this._rawAsyncValidators));
}
/**
* Check whether a synchronous validator function is present on this control. The provided
* validator must be a reference to the exact same function that was provided.
*
* @param validator The validator to check for presence. Compared by function reference.
* @returns Whether the provided validator was found on this control.
*/
hasValidator(validator: ValidatorFn): boolean {
return hasValidator(this._rawValidators, validator);
}
/**
* Check whether an asynchronous validator function is present on this control. The provided
* validator must be a reference to the exact same function that was provided.
*
* @param validator The asynchronous validator to check for presence. Compared by function
* reference.
* @returns Whether the provided asynchronous validator was found on this control.
*/
hasAsyncValidator(validator: AsyncValidatorFn): boolean {
return hasValidator(this._rawAsyncValidators, validator);
}
/**
* Empties out the synchronous validator list.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
@@ -1065,7 +1155,7 @@ export abstract class AbstractControl {
* console.log(control.status); // 'DISABLED'
* ```
*
* The following example initializes the control with a sync validator.
* The following example initializes the control with a synchronous validator.
*
* ```ts
* const control = new FormControl('', Validators.required);
+53
View File
@@ -663,3 +663,56 @@ export function getControlAsyncValidators(control: AbstractControl): AsyncValida
AsyncValidatorFn[]|null {
return (control as any)._rawAsyncValidators as AsyncValidatorFn | AsyncValidatorFn[] | null;
}
/**
* Accepts a singleton validator, an array, or null, and returns an array type with the provided
* validators.
*
* @param validators A validator, validators, or null.
* @returns A validators array.
*/
export function makeValidatorsArray<T extends ValidatorFn|AsyncValidatorFn>(validators: T|T[]|
null): T[] {
if (!validators) return [];
return Array.isArray(validators) ? validators : [validators];
}
/**
* Determines whether a validator or validators array has a given validator.
*
* @param validators The validator or validators to compare against.
* @param validator The validator to check.
* @returns Whether the validator is present.
*/
export function hasValidator<T extends ValidatorFn|AsyncValidatorFn>(
validators: T|T[]|null, validator: T): boolean {
return Array.isArray(validators) ? validators.includes(validator) : validators === validator;
}
/**
* Combines two arrays of validators into one. If duplicates are provided, only one will be added.
*
* @param validators The new validators.
* @param currentValidators The base array of currrent validators.
* @returns An array of validators.
*/
export function addValidators<T extends ValidatorFn|AsyncValidatorFn>(
validators: T|T[], currentValidators: T|T[]|null): T[] {
const current = makeValidatorsArray(currentValidators);
const validatorsToAdd = makeValidatorsArray(validators);
validatorsToAdd.forEach((v: T) => {
// Note: if there are duplicate entries in the new validators array,
// only the first one would be added to the current list of validarors.
// Duplicate ones would be ignored since `hasValidator` would detect
// the presence of a validator function and we update the current list in place.
if (!hasValidator(current, v)) {
current.push(v);
}
});
return current;
}
export function removeValidators<T extends ValidatorFn|AsyncValidatorFn>(
validators: T|T[], currentValidators: T|T[]|null): T[] {
return makeValidatorsArray(currentValidators).filter(v => !hasValidator(validators, v));
}