77

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.

2
  • Maybe this approach could work? Commented Oct 18, 2018 at 2:22
  • I want to apply conditional required validation on my licenseNo field based on the value of age. Commented Oct 18, 2018 at 2:24

11 Answers 11

135

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.

Sign up to request clarification or add additional context in comments.

6 Comments

we can put updateValueAndValidity() outside the if else statement , we don't have to repeat it.
hi! This code will be in the ngOnInit() method?
Unfortunately, i hope it will just work with [required]="someCondition()" but faced strange issues, thanks ! your solution saved me
I tried the same thing it didn't work, after 1 hour I came to this answer and tried this again and it worked :D Lol
see my answer, you should now addValidators and removeValidators
|
25

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

1 Comment

I believe you will also need to call updateValueAndValidity In above case it would be this.userCustomForm.get('licenseNo').updateValueAndValidity();
9

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

2 Comments

how will you access the above field age in your first approach? that's the answer being searched I guess
this does not add/remove the validators dynamically depending on the age field. it just sets it initally
7

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

6 Comments

Nice solution, working fine, even with multiple conditional validators. Btw, you can replace eval with Function as described here developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…! Which means the if will become if (Function(`"use strict"; return '${masterControl.value}' ${operator} '${conditionalValue}'`)())
@Dimitris Thank you for the explanation, at the time of writing I didn't pay attention to the eval problem. I also, honestly, didn't know about Function that can remplace Eval. Today is a good day ^^ I don't have an angular working project to check your fix. I trust you on this one, just tell me if I made a typo replacing the if you pointed me out.
This looks slick. i am trying to make this work with a checkbox such that once checked, the mat-datepicker will be required. the current function doesnt work for this case scenario. what could be missing?
solution seems good; language is problematic
@chitgoks I'm a bit late and I don't works with angular anymore but I would first try to check if my checkbox state is up to date. For the conditionnal part it should works the same as javascript so if your control name is 'checkbox' then I would try with conditionallyRequiredValidator('checkbox', '===', true, 'whatEverTheControlToBeRequiredAfterThat')
|
6

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

Comments

4

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

Comments

3

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

Comments

2

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

1 Comment

This is what I've been looking for for quite a while! It works like a charm!
1

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

Comments

0

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[];
  };
}

Comments

0

So many answers, but the solution using a simple custom validator is missing.

const customValidator: ValidatorFn = (control: AbstractControl) => {
  const group = control.parent as (FormGroup | null);
  if (group && group.controls["age"].value >= 18) {
    return Validators.required(control);
  }
  return null;
}

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.