3
votes

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

4

4 Answers

0
votes

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
});
0
votes

Try the following code.

this.profileForm.get('DateOfBirth2').setValidators([Validators.required]);
this.profileForm.get('DateOfBirth2').updateValueAndValidity();
0
votes

Using distinctUntilChanged from rxjs/operators will solve the Maximum call stack size exceeded error.

Change the line from

this.profileForm.get('NameNumber2').valueChanges.subscribe(() => {

to

this.profileForm.get('NameNumber2').valueChanges.pipe(distinctUntilChanged()).subscribe(() => {

So, the overall code will change as like below.

import { distinctUntilChanged } from 'rxjs/operators';

this.profileForm.get('NameNumber2').valueChanges.pipe(distinctUntilChanged()).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.pipe(distinctUntilChanged()).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();
});

I ran the above changed code, The form is valid and submit button is enabled for all the scenarios you mentioned.

0
votes

Why not use a customValidator over all the form? You emit differents errors and check the error over form. A function auxiliar indicate you waht fields has errors Some like:

  form=new FormGroup({
    name1:new FormControl(),
    date1:new FormControl(),
    compare:new FormControl(),
    name2:new FormControl(),
    date2:new FormControl(),
  },this.customValidator())

  hasError(error:string)
  {
    return this.form.errors?this.form.errors.error.find(x=>x==error):null
  }
  customValidator()
  {
    return (form:FormGroup)=>{
      const errors=[];
      if (!form.value.compare)
      {
        if (!form.value.name1)
            errors.push('name1')
        if (!form.value.date1)
            errors.push('date1')
      }
      else
      {
          ....
      }
      return errors.length?{error:errors}:null
    }
  }

And your form like

<form [formGroup]="form">
  <input formControlName="name1"/>
  <span *ngIf="hasError('name1')">*</span>

  <input formControlName="date1"/>
  <span *ngIf="hasError('date1')">*</span>
  <br/>
  <input type="checkbox" formControlName="compare"/>
  <br/>
  <input *ngIf="form.get('compare').value" formControlName="name2"/>
  <span *ngIf="hasError('name2')">*</span>
  <input *ngIf="form.get('compare').value" formControlName="date2"/>
    <span *ngIf="hasError('date2')">*</span>
</form>

Another idea is similar, has a customValidator that return always null, but use setErrors to give manually an error to your fields

  customValidator()
  {
    return (form:FormGroup)=>{
      const errors=[];
      if (!form.value.compare)
      {
        if (!form.value.name1)
            errors.push('name1')
        if (!form.value.date1)
            errors.push('date1')
      }
      else
      {
         ....other logic...
      }
      form.get('name1').setErrors(errors.find(x=>x=='name1')?{error:"required"}:null)
      form.get('date1').setErrors(errors.find(x=>x=='date1')?{error:"required"}:null)
      form.get('name2').setErrors(errors.find(x=>x=='name2')?{error:"required"}:null)
      form.get('date2').setErrors(errors.find(x=>x=='date2')?{error:"required"}:null)
      return null
    }
  }