Search code examples
angulartypescriptvalidationionic4angular-forms

Dynamically add/remove validators based on condition


Scenario:

Initially I have one text box(Name1), one date picker(DOB1) and a check box (Compare). Both Name1 and DOB1 are required. When check box is clicked, two more form controls are dynamically added named as Name2 and DOB2 and either any one of Name1 or DOB2 are required.

so the valid form is having any of

  1. Name1 DOB1 Name2 or //If Name2 is valid then need to remove required validator from DOB2
  2. Name1 DOB1 DOB2 or //If DOB2 is valid then need to remove required validator from Name2
  3. Name1 DOB1 Name2 DOB2

In all the above cases, the form is valid show enable the submit button.

Issue:

I had tried using setValidators but still couldn't figure out what i'm missing. When I click the checkbox, the form is valid only when all four controls are valid. I just need any three of them valid.

Code:

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <ion-card class="person1">
    <ion-card-content>
      <ion-list lines="full" class="ion-no-margin ion-no-padding">
        <ion-item>
          <ion-label position="stacked">Name / Number <ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-input type="text" formControlName="NameNumber"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="stacked">Date of birth<ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-datetime required placeholder="Select Date" formControlName="DateOfBirth"></ion-datetime>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
  <ion-card class="person2" *ngIf="isComparisonChecked">
    <ion-card-content>
      <ion-list lines="full" class="ion-no-margin ion-no-padding">
        <ion-item>
          <ion-label position="stacked">Name / Number <ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-input type="text" formControlName="NameNumber2"></ion-input>
        </ion-item>
        <ion-item>
          <ion-label position="stacked">Date of birth<ion-text color="danger">*</ion-text>
          </ion-label>
          <ion-datetime required placeholder="Select Date" formControlName="DateOfBirth2"></ion-datetime>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
  <ion-item class="compare-section" lines="none">
    <ion-label>Compare</ion-label>
    <ion-checkbox color="danger" formControlName="IsCompare"></ion-checkbox>
  </ion-item>
  <div class="ion-padding">
    <ion-button color="danger" *ngIf="LicensedStatus" [disabled]="!this.profileForm.valid" expand="block"
      type="submit" class="ion-no-margin">Submit</ion-button>
  </div>
</form>

Ts:

profileForm = new FormGroup({
NameNumber: new FormControl('', [Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]),
DateOfBirth: new FormControl('', Validators.required),
IsCompare: new FormControl(false)
});
...
this.profileForm.get('IsCompare').valueChanges.subscribe(checked => {
if (checked) {
    this.profileForm.addControl('NameNumber2', new FormControl('', [Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]));
    this.profileForm.addControl('DateOfBirth2', new FormControl('', Validators.required));

    this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
      if (this.profileForm.get('NameNumber2').valid) {
        this.profileForm.get('DateOfBirth2').clearValidators();
      }
      else {
        this.profileForm.get('DateOfBirth2').setValidators([Validators.required]);
      }
    this.profileForm.get('DateOfBirth2').updateValueAndValidity();
    });

    this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
      if (this.profileForm.get('DateOfBirth2').valid) {
        this.profileForm.get('NameNumber2').clearValidators();
      }
      else {
        this.profileForm.get('NameNumber2').setValidators([Validators.required, Validators.pattern('^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$')]);
      }
    this.profileForm.get('NameNumber2').updateValueAndValidity();
    });
  }
  else {
    this.profileForm.removeControl('NameNumber2');
    this.profileForm.removeControl('DateOfBirth2');
  }
});

What am I missing here?

Update #1:

I have updated the above code. If I use updateValueAndValidity i'm getting this error in the console

enter image description here


Solution

  • This is happening because updateValueAndValidity() emits another valueChanges event. So your subscriptions trigger each other infinitely.

    this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
      // omitted
      this.profileForm.get('DateOfBirth2').updateValueAndValidity(); // Triggers valueChanges for 'DateOfBirth2' 
    });
    
    this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
      // omitted
      this.profileForm.get('NameNumber2').updateValueAndValidity(); // Triggers valueChanges for 'NameNumber2' 
    });
    

    One way to avoid this is already described in previous posts: Using distinctUntilChanged.

    A cleaner way is built into the method itself though: updateValueAndValidity() takes an object to configure its behaviour. updateValueAndValidity({emitEvent: false}) will prevent the valueChanges event to be emitted and thus stop the event loop.

    this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {
      // omitted
      this.profileForm.get('DateOfBirth2').updateValueAndValidity({emitEvent:false}); // Does NOT trigger valueChanges
    });
    
    this.profileForm.get('DateOfBirth2').valueChanges.subscribe(() => {
      // omitted
      this.profileForm.get('NameNumber2').updateValueAndValidity({emitEvent:false}); // Does NOT trigger valueChanges
    });