Home > database >  Custom Validator in Angular Reactive Forms not working
Custom Validator in Angular Reactive Forms not working

Time:05-25

I have a custom validator on the field postalCode:

function postalCodeValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!this.contactForm) {
      return null;
    }

    const isPotalCodeRequired = this.contactForm.get(
      'isPotalCodeRequired'
    ).value;

    if (isPotalCodeRequired && !control.value) {
      console.log('here');
      this.contactForm.controls['postalCode'].setErrors({ required: true });
    } else {
      this.contactForm.controls['postalCode'].setErrors({ required: false });
    }

    return null;
  };
}

which checks another field isPotalCodeRequired to see if validation should be applied to the postalCode field or not.

If isPotalCodeRequired is true, a value is required for postalCode else it can be left empty. But my custom validation doesn't seem to work as expected when I call setErrors on the postalCode field. It adds it, within the custom validator function, but checking it after the function executes, the error is no longer present on the postalCode field.

Demo.

CodePudding user response:

Angular's validator functions are a little bit weird. You need to return null when the control has no errors and an object containing the errors and a brief description when they are incorrect:

function postalCodeValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!this.contactForm) {
      return null;
    }

    const isPotalCodeRequired = this.contactForm.get(
      'isPotalCodeRequired'
    ).value;

    if (isPotalCodeRequired && !control.value) {
      console.log('here');
      return {required: 'Postal code is required' };
    }

    return null;
  };
}

You don't need to manually set the errors as the framework will do it for you

CodePudding user response:

What you are trying to do is called cross-field validation and typically with a cross-field validator you should be applying the validator the FormGroup and not to the FormControl. Here,

this.contactForm = this.formBuilder.group({
  isPotalCodeRequired: [true],
  postalCode: [null, Validators.compose([postalCodeValidator.call(this)])],
});

you are binding your postalCodeValidator to a specific control. When you apply a validator like this, that validator is expected to return the validation messages that you want applied to the control but you are returning null,

  if (isPotalCodeRequired && !control.value) {
    console.log('here');
    this.contactForm.controls['postalCode'].setErrors({ required: true });
  } else {
    this.contactForm.controls['postalCode'].setErrors({ required: false });
  }

  return null;

Which clears all he validation messages that are applied to that control. Instead, bind this validator to the FormGroup containing both of your controls,

this.contactForm = this.formBuilder.group({
  isPotalCodeRequired: [true],
  postalCode: [null]
}, {validators: Validators.compose([postalCodeValidator.call(this)])});

Now the AbstractControl that gets passed into the validator is your entire FormGroup. This has the benefit of providing access to all of the controls that you need. So instead of references to this.contactForm you would have,

control.controls['postalCode'].setErrors({ required: true });

You should be able to remove all references to this.contactForm from inside the validator. More importantly, your null return is no longer going to clear the validation messages on the individual control. Check out the Angular docs on cross-field validation for reactive forms.

But there is a cleaner way to do this altogether. Instead of writing a custom validator, you can just listen for changes on isPostalCodeRequired and add/remove the built-in required validator as needed,

this.contactForm.get('isPostalCodeRequired').valueChanges.subscribe(val => {
  if (val) { 
    //Add validator
  else {
    //Remove validator
  })

The helper functions available to add/remove validators depend on your Angular version but they are pretty straightforward.

EDIT: Updated the answer to address the code in the demo.

  • Related