Skip to content

Commit

Permalink
fix(forms): don't mutate validators array (#47830)
Browse files Browse the repository at this point in the history
Fixes that the `AbstractControl` was mutating the validators arrays being passed into the constructor an helper methods like `setValidators`.

Fixes #47827.

PR Close #47830
  • Loading branch information
crisbeto authored and dylhunn committed Nov 17, 2022
1 parent c7b1cc0 commit b342e55
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 24 deletions.
Expand Up @@ -632,12 +632,6 @@
{
"name": "cleanUpView"
},
{
"name": "coerceToAsyncValidator"
},
{
"name": "coerceToValidator"
},
{
"name": "collectNativeNodes"
},
Expand Down
Expand Up @@ -608,15 +608,9 @@
{
"name": "cleanUpView"
},
{
"name": "coerceToAsyncValidator"
},
{
"name": "coerceToBoolean"
},
{
"name": "coerceToValidator"
},
{
"name": "collectNativeNodes"
},
Expand Down
40 changes: 28 additions & 12 deletions packages/forms/src/model/abstract_model.ts
Expand Up @@ -372,15 +372,15 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
*
* @internal
*/
private _composedValidatorFn: ValidatorFn|null;
private _composedValidatorFn!: ValidatorFn|null;

/**
* Contains the result of merging asynchronous validators into a single validator function
* (combined using `Validators.composeAsync`).
*
* @internal
*/
private _composedAsyncValidatorFn: AsyncValidatorFn|null;
private _composedAsyncValidatorFn!: AsyncValidatorFn|null;

/**
* Synchronous validators as they were provided:
Expand All @@ -390,7 +390,7 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
*
* @internal
*/
private _rawValidators: ValidatorFn|ValidatorFn[]|null;
private _rawValidators!: ValidatorFn|ValidatorFn[]|null;

/**
* Asynchronous validators as they were provided:
Expand All @@ -401,7 +401,7 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
*
* @internal
*/
private _rawAsyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null;
private _rawAsyncValidators!: AsyncValidatorFn|AsyncValidatorFn[]|null;

/**
* The current value of the control.
Expand All @@ -427,10 +427,8 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
constructor(
validators: ValidatorFn|ValidatorFn[]|null,
asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null) {
this._rawValidators = validators;
this._rawAsyncValidators = asyncValidators;
this._composedValidatorFn = coerceToValidator(this._rawValidators);
this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators);
this._assignValidators(validators);
this._assignAsyncValidators(asyncValidators);
}

/**
Expand Down Expand Up @@ -620,8 +618,7 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
* using `addValidators()` method instead.
*/
setValidators(validators: ValidatorFn|ValidatorFn[]|null): void {
this._rawValidators = validators;
this._composedValidatorFn = coerceToValidator(validators);
this._assignValidators(validators);
}

/**
Expand All @@ -635,8 +632,7 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
* using `addAsyncValidators()` method instead.
*/
setAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]|null): void {
this._rawAsyncValidators = validators;
this._composedAsyncValidatorFn = coerceToAsyncValidator(validators);
this._assignAsyncValidators(validators);
}

/**
Expand Down Expand Up @@ -1377,4 +1373,24 @@ export abstract class AbstractControl<TValue = any, TRawValue extends TValue = T
_find(name: string|number): AbstractControl|null {
return null;
}

/**
* Internal implementation of the `setValidators` method. Needs to be separated out into a
* different method, because it is called in the constructor and it can break cases where
* a control is extended.
*/
private _assignValidators(validators: ValidatorFn|ValidatorFn[]|null): void {
this._rawValidators = Array.isArray(validators) ? validators.slice() : validators;
this._composedValidatorFn = coerceToValidator(this._rawValidators);
}

/**
* Internal implementation of the `setAsyncValidators` method. Needs to be separated out into a
* different method, because it is called in the constructor and it can break cases where
* a control is extended.
*/
private _assignAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]|null): void {
this._rawAsyncValidators = Array.isArray(validators) ? validators.slice() : validators;
this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators);
}
}
44 changes: 44 additions & 0 deletions packages/forms/test/form_control_spec.ts
Expand Up @@ -266,6 +266,16 @@ describe('FormControl', () => {
expect(c.valid).toEqual(true);
});

it('should not mutate the validators array when overriding using setValidators', () => {
const control = new FormControl('');
const originalValidators = [Validators.required];

control.setValidators(originalValidators);
control.addValidators(Validators.minLength(10));

expect(originalValidators.length).toBe(1);
});

it('should override validators by setting `control.validator` field value', () => {
const c = new FormControl('');
expect(c.valid).toEqual(true);
Expand Down Expand Up @@ -357,6 +367,30 @@ describe('FormControl', () => {
expect(c.hasValidator(Validators.required)).toEqual(false);
});

it('should not mutate the validators array when adding/removing sync validators', () => {
const originalValidators = [Validators.required];
const control = new FormControl('', originalValidators);

control.addValidators(Validators.min(10));
expect(originalValidators.length).toBe(1);

control.removeValidators(Validators.required);
expect(originalValidators.length).toBe(1);
});

it('should not mutate the validators array when adding/removing async validators', () => {
const firstValidator = asyncValidator('one');
const secondValidator = asyncValidator('two');
const originalValidators = [firstValidator];
const control = new FormControl('', null, originalValidators);

control.addAsyncValidators(secondValidator);
expect(originalValidators.length).toBe(1);

control.removeAsyncValidators(firstValidator);
expect(originalValidators.length).toBe(1);
});

it('should return false when checking presence of a validator not identical by reference',
() => {
const minValidator = Validators.min(5);
Expand Down Expand Up @@ -518,6 +552,16 @@ describe('FormControl', () => {
expect(c.valid).toEqual(true);
}));

it('should not mutate the validators array when overriding using setValidators', () => {
const control = new FormControl('');
const originalValidators = [asyncValidator('one')];

control.setAsyncValidators(originalValidators);
control.addAsyncValidators(asyncValidator('two'));

expect(originalValidators.length).toBe(1);
});

it('should override validators by setting `control.asyncValidator` field value',
fakeAsync(() => {
const c = new FormControl('');
Expand Down

0 comments on commit b342e55

Please sign in to comment.