vinit tyagi
vinit tyagi

Reputation: 973

Conditional required validation in angular reactive form

I want to apply conditional validation on some properties based on some other form values. I have referred some answers Angular2: Conditional required validation, but those are not fulfil my need. Because I have to implement conditional validation in 40+ form(around 30 fields) of my large enterprise application. I don't want write the same code in every component and change the FormControl name. I don't know how this can be achieved via Directive.

if age control valuev is greater than 18 than the license number field is required.

Here's my code:

this.userCustomForm = this.angularFormBuilder.group({
age:['',Validators.required],
licenseNo:[''] // Here I want to apply conditional required validation.
});

In my application there are some cases where I want set conditional validation based on nested FormGroup or FormArray values.

Please guide me, how can I achieve this.

Upvotes: 74

Views: 116383

Answers (10)

Ram Chander
Ram Chander

Reputation: 1518

Solution 1

You can try the following way as well

this.userCustomForm = this.angularFormBuilder.group({
    age:['',Validators.required],
    licenseNo:['', condition ? Validators.required:Validators.nullValidator ] 
});

Solution 2

You can achive the same by following

this.userCustomForm = this.angularFormBuilder.group({
    age: ['', Validators.required],
    licenseNo: ['']
});


handleValidations(condition: any){
    if (condition) {
        this.pushNotificationForm.get('licenseNo').setValidators([Validators.required]);
    } else {
        this.pushNotificationForm.get('licenseNo').setValidators([Validators.nullValidator]);;
    }
    this.pushNotificationForm.get('licenseNo').updateValueAndValidity();
}

Upvotes: 7

Mark Lagendijk
Mark Lagendijk

Reputation: 6923

To make this work nicely it is best if the solution is dynamic and robust, so it can easily and safely be used in any situation.

We can achieve this by creating a validator function on FormGroup level that, instead of validating, adds and removes the validator(s) on the child control based on a condition function.

It is used as follows:

this.userCustomForm = this.angularFormBuilder.group({
  age:['',Validators.required],
  licenseNo:['']
}, {
  validators: [
    conditionalValidators({
      licenseNo: {
        condition: (formValue) => formValue.age > 18,
        validators: [Validators.required]
      }
    })
  ]
});

The implementation is quite concise and straightforward:

export function conditionalValidators(groupValidators: FormGroupConditionalValidators): ValidatorFn {
  return (formGroup: FormGroup) => {
    for (const controlName in groupValidators) {
      const control = formGroup.controls[controlName];
      const { condition, validators } = groupValidators[controlName];
      updateValidators(control, condition, validators, formGroup.value);
    }

    return null;
  };
}

function updateValidators(
  control: AbstractControl, condition: (formGroupValue: any) => boolean, 
  validators: ValidatorFn[], formGroupValue: any) {
  if (condition(formGroupValue)) {
    addValidators(control, validators);
  } else {
    removeValidators(control, validators);
  }
}

function addValidators(control: AbstractControl, validators: ValidatorFn[]) {
  if (validators.some((validator) => !control.hasValidator(validator))) {
    control.addValidators(validators);
    control.updateValueAndValidity({ onlySelf: true });
  }
}

function removeValidators(control: AbstractControl, validators: ValidatorFn[]) {
  if (validators.some((validator) => control.hasValidator(validator))) {
    control.removeValidators(validators);
    control.updateValueAndValidity({ onlySelf: true });
  }
}

export interface FormGroupConditionalValidators {
  [controlName: string]: {
    condition: (formGroupValue: any) => boolean;
    validators: ValidatorFn[];
  };
}

Upvotes: 0

Jack A.
Jack A.

Reputation: 4453

We use this form-level validator to add or remove the Validators.required validator on an input based on a condition:

export const maybeRequiredValidator = (
    inputName: string,
    requiredWhen: (form: AbstractControl) => boolean
): ValidatorFn =>
    (form: AbstractControl): ValidationErrors | null => {
        let targetInput = form.get(inputName);
        if (targetInput) {
            let isRequired = requiredWhen(form);
            if (isRequired != targetInput.hasValidator(Validators.required)) {
                if (isRequired) {
                    targetInput.addValidators(Validators.required);
                }
                else {
                    targetInput.removeValidators(Validators.required);
                }
                targetInput.updateValueAndValidity({ onlySelf: true });
            }
        }
        return null;
    };

This lets you set the required status of an input based on any number of other form fields.

Usage example based on the OP:

this.userCustomForm = this.angularFormBuilder.group({
  age:['',Validators.required],
  licenseNo:['']
}, {
  validators: [
    maybeRequiredValidator('licenseNo', form => parseInt(form.get('age').value) > 18)
  ]
});

EDIT

I ended up needing a more generic version of this, one that could handle multiple validators and validators other than required. So I created this:

/**
 * @description
 * Form-level validator that adds or removes validators on a specified input by 
 * evaluating a condition for each validator.
 * @usageNotes
 * This must be registered as a form-level validator.
 * ### Example
 * ```typescript
 * let form = new FormGroup({}, {
 *     validators: conditionalValidators(
 *         'companyName',
 *         [
 *             {
 *                 isSelected: form => form.get('addressType').value == 'business',
 *                 validators: Validators.required // or [Validators.required, ...]
 *             },
 *             // additional validators may be added here
 *         ]
 *     )
 * });
 * ```
 * @param inputName Name of the input that will have validators added or removed.
 * @param validatorSelectors Array of objects that include a function and validator(s).
 *        The function receives the form and returns a Boolean value that indicates
 *        if the validator(s) should be applied to the input.
 * @returns Validator function.
 */
export const conditionalValidators = (
    inputName: string,
    validatorSelectors: ReadonlyArray<ValidatorSelector>
): ValidatorFn =>
    (form: AbstractControl): ValidationErrors | null => {
        let targetInput = form.get(inputName);
        if (targetInput) {
            let anyChanges = false;
            for (let selector of validatorSelectors) {
                let isSelected = selector.isSelected(form);
                let validators = selector.validators instanceof Array 
                    ? selector.validators : [selector.validators];
                for (let validator of validators) {
                    if (isSelected != targetInput.hasValidator(validator)) {
                        anyChanges = true;
                        if (isSelected) {
                            targetInput.addValidators(validator);
                        }
                        else {
                            targetInput.removeValidators(validator);
                        }
                    }
                }
            }
            if (anyChanges) {
                targetInput.updateValueAndValidity({ onlySelf: true });
            }
        }
        return null;
    };

/**
 * Function that returns a Boolean indicating if validator(s) should be
 * applied to a form control.
 * @param form The form that contains the target form control.
 * @returns True if the validator(s) should be applied, else false.
 */
export interface ValidatorSelectionFn {
    (form: AbstractControl): boolean;
};

/**
 * Type used to conditionally select validator(s) for a form control.
 */
 export interface ValidatorSelector {
    /**
     * Function that returns a Boolean indicating if the validator(s) should be
     * applied to the form control.
     */
    readonly isSelected: ValidatorSelectionFn;
    /**
     * Single validator or array of validators applied to the form control when
     * isSelected returns true.
     */
    readonly validators: ValidatorFn | ReadonlyArray<ValidatorFn>;
};

Upvotes: 2

endlacer
endlacer

Reputation: 175

This extends on the most upvoted answer: Since Angular 12 you can use addValidators and removeValidators. This is an advantage over setValidators and clearValidators because it does not affect any other validators on the control. So it comes down to:

this.userCustomForm.get('age').valueChanges.subscribe(val => {
  if (condition) {
    this.userCustomForm.controls['licenseNo'].addValidators(Validators.required);
  } else {
    this.userCustomForm.controls['licenseNo'].removeValidators(Validators.required);
  }
  this.userCustomForm.controls['licenseNo'].updateValueAndValidity();
});

Upvotes: 3

Justus Metzger
Justus Metzger

Reputation: 1331

For me it worked perfectly like this:

this.userCustomForm.get('age').valueChanges.subscribe(val => {
  if (condition) {
    this.userCustomForm.controls['licenseNo'].setValidators([Validators.required]);
  } else {
    this.userCustomForm.controls['licenseNo'].clearValidators();
  }
  this.userCustomForm.controls['licenseNo'].updateValueAndValidity();
});

You have to updateValueAndValidity of the form for the changes to take effect.

Upvotes: 133

ARKA BANERJEE
ARKA BANERJEE

Reputation: 11

you can use the following approach:

In template add :

<input formcontrol="age" (input)="changeEvent()">

In ts add:

  changeEvent(){
    if (this.userCustomForm.get('age').value>12) {
    this.userCustomForm.controls['licenseNo'].setValidators([Validators.required]);
  } else {
    this.userCustomForm.controls['licenseNo'].clearValidators();
  }
  this.userCustomForm.controls['licenseNo'].updateValueAndValidity();
}

Upvotes: 1

D3F
D3F

Reputation: 181

There is a more generic approch which can be use for multiple purpose, not just this one.

Each time you want to conditionally add the Validators.required to a control you can use this function.

First create this function (in a service should be the best idea because it's generic, so you can use it later with different conditions in a different component, but for the example it's in the same component)

import { FormGroup, Validators } from '@angular/forms';

conditionallyRequiredValidator(masterControlLabel: string, operator: string, conditionalValue: any, slaveControlLabel: string) {
  return (group: FormGroup): {[key: string]: any} => {
    const masterControl = group.controls[masterControlLabel];
    const slaveControl = group.controls[slaveControlLabel];     
    if (Function(`"use strict"; return '${masterControl.value}' ${operator} '${conditionalValue}'`)()) { 
      return Validators.required(slaveControl)
    }
    slaveControl.setErrors(null); 
    return null;
  }
}

masterControlLabel: the control which will conditionally add the Validators.required to the slaveControl

operator: the operator you want to use to compare the masterControl value with the conditionalValue

conditionalValue: what ever the value you want the masterControl value must match to conditionally add the validator to the slaveControl

slaveControlLabel: the control which will receive (or not) the conditonal Validators.required

Second and finally, add the validator parameter (or validators parameter if mutliple validations need to be done) in your formGroup like this :

import { FormBuilder, FormGroup, Validators } from "@angular/forms";

constructor(
  private angularFormBuilder: FormBuilder,
){ }

myFormGroup: FormGroup = this.angularFormBuilder.group({
  age: ['', Validators.required],
  licenceNo: [''],
}, {validator: conditionallyRequiredValidator('age', '>=', 18, 'licenceNo')});

In this example if the age is strictly over or egal to 18 then the licenceNo control will be conditionally required

Upvotes: 9

Timothy
Timothy

Reputation: 3593

Conditionally set validator via setValidators method of FormControl class, ie

this.userCustomForm = this.angularFormBuilder.group({
    age:['', Validators.required],
    licenseNo:['']
});

if (condition) {
    this.userCustomForm.get('licenseNo').setValidators([
        Validators.required
    ]);
}

Upvotes: 3

Ousama
Ousama

Reputation: 2810

I solved this problem by doing this :

this.userCustomForm = new FormGroup({
    age: new FormControl('',Validators.required)
    licenseNo: new FormControl('', condition ? Validators.required : [])
});

There are another way to do this by using setValidators and clearValidators methods please see the following example:

if(condition) {
    this.userCustomForm.get('licenseNo').setValidators(Validators.required);
} else {
    this.userCustomForm.get('licenseNo').clearValidators();
}

Upvotes: 9

Franklin Pious
Franklin Pious

Reputation: 3858

My suggestion would be to use dynamic validations.

Subscribe to the changes in the age field of the userCustomForm and whenever the age reaches the condition where license needs to validated, add validators.required dynamically using setValidators() and clear the validators dynamically using clearValidators() whenever necessary.

    this.userCustomForm.get('age').valueChanges.subscribe(val => {
        if (condition) { // for setting validations
          this.userCustomForm.get('licenseNo').setValidators(Validators.required);
        } 
        if (condition) { // for clearing validations
          this.userCustomForm.get('licenseNo').clearValidators();
        }
        this.userCustomForm.get('licenseNo').updateValueAndValidity();
    });

Upvotes: 25

Related Questions