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

View File

@ -21,6 +21,8 @@ import { Version } from '@angular/core';
// @public
export abstract class AbstractControl {
constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null);
addAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void;
addValidators(validators: ValidatorFn | ValidatorFn[]): void;
get asyncValidator(): AsyncValidatorFn | null;
set asyncValidator(asyncValidatorFn: AsyncValidatorFn | null);
clearAsyncValidators(): void;
@ -39,7 +41,9 @@ export abstract class AbstractControl {
readonly errors: ValidationErrors | null;
get(path: Array<string | number> | string): AbstractControl | null;
getError(errorCode: string, path?: Array<string | number> | string): any;
hasAsyncValidator(validator: AsyncValidatorFn): boolean;
hasError(errorCode: string, path?: Array<string | number> | string): boolean;
hasValidator(validator: ValidatorFn): boolean;
get invalid(): boolean;
markAllAsTouched(): void;
markAsDirty(opts?: {
@ -62,15 +66,17 @@ export abstract class AbstractControl {
abstract patchValue(value: any, options?: Object): void;
get pending(): boolean;
readonly pristine: boolean;
removeAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void;
removeValidators(validators: ValidatorFn | ValidatorFn[]): void;
abstract reset(value?: any, options?: Object): void;
get root(): AbstractControl;
setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null): void;
setAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[] | null): void;
setErrors(errors: ValidationErrors | null, opts?: {
emitEvent?: boolean;
}): void;
// (undocumented)
setParent(parent: FormGroup | FormArray): void;
setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void;
setValidators(validators: ValidatorFn | ValidatorFn[] | null): void;
abstract setValue(value: any, options?: Object): void;
readonly status: string;
readonly statusChanges: Observable<any>;

View File

@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1150,
"main-es2015": 162346,
"main-es2015": 163007,
"polyfills-es2015": 36975
}
}

View File

@ -665,6 +665,9 @@
{
"name": "addToViewTree"
},
{
"name": "addValidators"
},
{
"name": "allocExpando"
},
@ -1082,6 +1085,9 @@
{
"name": "hasValidLength"
},
{
"name": "hasValidator"
},
{
"name": "hostReportError"
},
@ -1271,6 +1277,9 @@
{
"name": "makeRecord"
},
{
"name": "makeValidatorsArray"
},
{
"name": "map"
},
@ -1430,6 +1439,9 @@
{
"name": "removeStyle"
},
{
"name": "removeValidators"
},
{
"name": "renderComponent"
},

View File

@ -653,6 +653,9 @@
{
"name": "addToViewTree"
},
{
"name": "addValidators"
},
{
"name": "allocExpando"
},
@ -1046,6 +1049,9 @@
{
"name": "hasTagAndTypeMatch"
},
{
"name": "hasValidator"
},
{
"name": "hostReportError"
},
@ -1232,6 +1238,9 @@
{
"name": "makeRecord"
},
{
"name": "makeValidatorsArray"
},
{
"name": "map"
},
@ -1394,6 +1403,9 @@
{
"name": "removeStyle"
},
{
"name": "removeValidators"
},
{
"name": "renderComponent"
},

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);

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));
}

View File

@ -69,6 +69,17 @@ describe('Form Builder', () => {
expect(g.controls['login'].asyncValidator).toBe(asyncValidator);
});
it('should support controls with validators that are later modified', () => {
const g = b.group({'login': b.control(null, syncValidator, asyncValidator)});
expect(g.controls['login'].value).toBeNull();
expect(g.controls['login'].validator).toBe(syncValidator);
expect(g.controls['login'].asyncValidator).toBe(asyncValidator);
g.controls['login'].addValidators(Validators.required);
expect(g.controls['login'].hasValidator(Validators.required)).toBe(true);
g.controls['login'].removeValidators(Validators.required);
expect(g.controls['login'].hasValidator(Validators.required)).toBe(false);
});
it('should support controls with no validators and whose form state is undefined', () => {
const g = b.group({'login': b.control(undefined)});
expect(g.controls['login'].value).toBeNull();

View File

@ -312,6 +312,57 @@ describe('FormControl', () => {
c.setValidators([Validators.required]);
expect(c.validator).not.toBe(null);
});
it('should check presence of and remove a validator set in the control constructor', () => {
const c = new FormControl('', Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(true);
c.removeValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(false);
c.addValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(true);
});
it('should check presence of and remove a validator set with addValidators', () => {
const c = new FormControl('');
expect(c.hasValidator(Validators.required)).toEqual(false);
c.addValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(true);
c.removeValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(false);
});
it('should check presence of and remove multiple validators set at the same time', () => {
const c = new FormControl('3');
const minValidator = Validators.min(4);
c.addValidators([Validators.required, minValidator]);
expect(c.hasValidator(Validators.required)).toEqual(true);
expect(c.hasValidator(minValidator)).toEqual(true);
c.removeValidators([Validators.required, minValidator]);
expect(c.hasValidator(Validators.required)).toEqual(false);
expect(c.hasValidator(minValidator)).toEqual(false);
});
it('should be able to remove a validator added multiple times', () => {
const c = new FormControl('', Validators.required);
c.addValidators(Validators.required);
c.addValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(true);
c.removeValidators(Validators.required);
expect(c.hasValidator(Validators.required)).toEqual(false);
});
it('should return false when checking presence of a validator not identical by reference',
() => {
const minValidator = Validators.min(5);
const c = new FormControl('1', minValidator);
expect(c.hasValidator(minValidator)).toEqual(true);
expect(c.hasValidator(Validators.min(5))).toEqual(false);
});
});
describe('asyncValidator', () => {
@ -508,6 +559,61 @@ describe('FormControl', () => {
tick();
expect(c.status).toEqual('DISABLED');
}));
it('should check presence of and remove a validator set in the control constructor', () => {
const asyncVal = asyncValidator('expected');
const c = new FormControl('', null, asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(true);
c.removeAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(false);
c.addAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(true);
});
it('should check presence of and remove a validator set with addValidators', () => {
const c = new FormControl('');
const asyncVal = asyncValidator('expected');
c.addAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(true);
c.removeAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(false);
});
it('should check presence of and remove multiple validators set at the same time', () => {
const c = new FormControl('3');
const asyncVal1 = asyncValidator('expected1');
const asyncVal2 = asyncValidator('expected2');
c.addAsyncValidators([asyncVal1, asyncVal2]);
expect(c.hasAsyncValidator(asyncVal1)).toEqual(true);
expect(c.hasAsyncValidator(asyncVal2)).toEqual(true);
c.removeAsyncValidators([asyncVal1, asyncVal2]);
expect(c.hasAsyncValidator(asyncVal1)).toEqual(false);
expect(c.hasAsyncValidator(asyncVal2)).toEqual(false);
});
it('should be able to remove a validator added multiple times', () => {
const asyncVal = asyncValidator('expected');
const c = new FormControl('', null, asyncVal);
c.addAsyncValidators(asyncVal);
c.addAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(true);
c.removeAsyncValidators(asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(false);
});
it('should return false when checking presence of a validator not identical by reference',
() => {
const asyncVal = asyncValidator('expected');
const asyncValDifferentFn = asyncValidator('expected');
const c = new FormControl('1', null, asyncVal);
expect(c.hasAsyncValidator(asyncVal)).toEqual(true);
expect(c.hasAsyncValidator(asyncValDifferentFn)).toEqual(false);
});
});
describe('dirty', () => {