angularvalidationcustomvalidatorformgroupsangular-abstract-control

Angular Reactive Form: Remove Specific Validator from Control


I am working with Angular reactive forms. In my scenario, validators are added dynamically to my controls.

This is done like follows:

const myControl = myFormGroup.get('myControl');

if (myControl.validator) {
    myControl.setValidators([ myControl.validator, Validators.required ]); //preserve existing validators
} else {
    myControl.setValidators([ Validators.required ]);
}

This works just fine.

The Problem

I need a way to also remove a specific validator while preserving the others.

myControl.validators only returns a function but not an array of validators which makes it impossible to pick one to remove while leaving the others.

I know there are workarounds like keeping track of the existing validators elswhere, etc. My question however is: can this be achieved directly on the AbstractControl?

The following issue on Github discusses the problem: https://github.com/angular/angular/issues/13461

The solutions provided there don't seem to work for me, as I am not only having to deal with the required validator but also with numerous custom validators.

Thanks in advance for any hints or solutions to this!

Cheers, Mike


Solution

  • Since there is no elegant solution to this, I have bitten the bullet to write my own RequiredIf validator.

    It does two things:

    Here's my code of the validator (just as I use it currently, no time to optimize it).

    import { FormGroup, ValidationErrors, Validators } from '@angular/forms';
    import { Subscription } from 'rxjs';
    
    export class ValidationSubscriptionCache {
        static observedPropertyCombinations = new Array<string>();
        static subscriptions = new Array<Subscription>();
    
        static unsubscribeAll() {
            for (let subscription of ValidationSubscriptionCache.subscriptions) {
                subscription.unsubscribe();
            }
        }
    }
    
    export function CombineProperties(property: string, otherProperty: string) {
        return `${ property }.${ otherProperty }`;
    }
    
    export class ValidatorCache {
        static cache = new Array<ValidatorsOfProperty>();
    
        static currentlyRequiredProperties = new Array<string>();
    
        static isCurrentlyRequired(property: string) {
            return ValidatorCache.currentlyRequiredProperties.indexOf(property) > -1;
        }
    
        static removeFromCurrentlyRequiredProperties(property: string) {
            const index = ValidatorCache.currentlyRequiredProperties.indexOf(property, 0);
    
            if (index > -1) {
                ValidatorCache.currentlyRequiredProperties.splice(index, 1);
            }
        }
    
        static isInCache(property: string) {
            const found = this.cache.find(x => x.property === property);
    
            if (found) {
                return true;
            }
    
            return false;
        }
    
        static addToCache(property: string, validators: any) {
            const toAdd = new ValidatorsOfProperty();
            toAdd.property = property;
            toAdd.validators = validators;
    
            ValidatorCache.cache.push(toAdd);
        }
    
        static popFromCache(property: string): ValidatorsOfProperty {
            const item = ValidatorCache.cache.find(x => x.property === property);
            const index = ValidatorCache.cache.indexOf(item, 0);
            ValidatorCache.cache.splice(index, 1);
    
            return item;
        }
    }
    
    export class ValidatorsOfProperty {
        property: string;
        validators: any;
    }
    
    export function ValidateRequiredIf(property: string, otherProperty: string, valueThatMakesRequired) {
        return (formGroup: FormGroup): ValidationErrors | null => {
    
            const propertyCombination = CombineProperties(property, otherProperty);
            const otherValue = formGroup.get(otherProperty).value;
    
            //register changes of other property and refresh validation if it changes
            if (ValidationSubscriptionCache.observedPropertyCombinations.indexOf(propertyCombination) < 0) {
                ValidationSubscriptionCache.observedPropertyCombinations.push(propertyCombination);
    
                const valueChangeSubscription =
                    formGroup.get(otherProperty).valueChanges
                        .subscribe(() => {
                            formGroup.get(property).markAsTouched();
                            formGroup.get(property).updateValueAndValidity();
                        });
    
                ValidationSubscriptionCache.subscriptions.push(valueChangeSubscription);
            }
    
            //set or remove the required validator based on the depending property
            const isRequired =
                otherValue !== null &&
                otherValue !== undefined &&
                otherValue.toString() === valueThatMakesRequired.toString();
    
            if (isRequired && !ValidatorCache.isCurrentlyRequired(property)) {
                ValidatorCache.currentlyRequiredProperties.push(property);
                ValidatorCache.addToCache(property, formGroup.get(property).validator);
    
                if (formGroup.get(property).validator) {
                    formGroup.get(property).setValidators([ formGroup.get(property).validator, Validators.required ]);
                } else {
                    formGroup.get(property).setValidators(Validators.required);
                }
    
            }
    
            if (!isRequired && ValidatorCache.isCurrentlyRequired(property)) {
                ValidatorCache.removeFromCurrentlyRequiredProperties(property);
                formGroup.get(property).setValidators(ValidatorCache.popFromCache(property).validators);
            }
    
            return null;
        };
    }
    

    And here's the application on the form:

    this.myForm.setValidators(
                        [
                            //city is required if the country is switzerland
                            ValidateRequiredIf('city', 'nationality', 'switzerland'),
                            //[add more]
                        ]);
    

    additionally, you have to call ValidationSubscriptionCache.unsubscribeAll(); in the ngOnDestroy() of your component holding the form.

    Cheers, Mike