user3005818
user3005818

Reputation: 81

Angular form control changing validation status without corresponding value change?

I have a mystery I am trying unravel with the following reactive form I am creating with the FormBuilder.

    this.form = this.fb.group({
      startDate: [this.startDate],
      startTime: [this.startTime],
      endDate: [this.endDate],
      endTime: [this.endTime]
    }, {validators: [this.validateDateRange()]});

In my attempts to debug, after form creation, I add several key Observers as follows:

public ngAfterViewInit() {
    this.form.get('startDate').valueChanges
      .pipe(
        distinctUntilChanged(),
        takeUntil(this.destroy$)
      )
      .subscribe((val) => {
          console.log(`startDate new value ${val}`);
          this.startDate = val;
          this.combineDateAndTime(true);
        },
        (err) => {
          console.log(`startDate error! ${JSON.stringify(err)}`);
        },
        () => {
          console.log(`startDate completed!`);
        });

    this.form.get('startDate').statusChanges
      .pipe(
        distinctUntilChanged(),
        takeUntil(this.destroy$)
      )
      .subscribe((val) => {
         console.log(`startDate is now ${val} [${JSON.stringify(this.form.get('startDate').errors)}] startDate: ${this.form.get('startDate').value}`);
      });

  this.form.statusChanges
    .pipe(
      distinctUntilChanged(),
      takeUntil(this.destroy$)
    )
    .subscribe((val) => {
      console.log(`form is now ${val} [${JSON.stringify(this.form.get('startDate').errors)}]`);
    });
}

Also, my cross field validator function looks like this:

  private validateDateRange(): ValidatorFn {
    return (group: FormGroup): ValidationErrors => {
      const now = moment().add(1, 'second');  // slight wiggle room for now based operations
      const startDate = this.startDate;
      const endDate = this.endDate;
      let errors;
      if (!startDate && !endDate) {
        errors = this.setFormControlError(group, 'startDate', 'required');
        errors = this.setFormControlError(group, 'startTime', 'required');
        errors = this.setFormControlError(group, 'endDate', 'required');
        errors = this.setFormControlError(group, 'endTime', 'required');
        return errors;
      }

      if (startDate && startDate.valueOf() > now.valueOf()) {
        errors = this.setFormControlError(group, 'startDate', 'maxDate');
        errors = this.setFormControlError(group, 'startTime', 'maxDate');
      } else if (!startDate && this.startTime) {
        errors = this.setFormControlError(group, 'startDate', 'required');
      } else {
        this.clearFormControlErrors(group, 'startDate', ['required', 'maxDate']);
        this.clearFormControlErrors(group, 'startTime', ['required', 'maxDate']);
      }
      if (endDate && endDate.valueOf() > now.valueOf()) {
        errors = this.setFormControlError(group, 'endDate', 'maxDate');
        errors = this.setFormControlError(group, 'endTime', 'maxDate');
      } else if (!endDate && this.endTime) {
        errors = this.setFormControlError(group, 'endDate', 'required');
      } else {
        this.clearFormControlErrors(group, 'endDate', ['required', 'maxDate']);
        this.clearFormControlErrors(group, 'endTime', ['required', 'maxDate']);
      }

      if (startDate && endDate) {
        if (startDate >= endDate) {
          errors = this.setFormControlError(group, 'startDate', 'maxDate');
          errors = this.setFormControlError(group, 'startTime', 'maxDate');
          errors = this.setFormControlError(group, 'endDate', 'maxDate');
          errors = this.setFormControlError(group, 'endTime', 'maxDate');
        }
      }
      return errors;
    };
  }

  private setFormControlError(group: FormGroup, controlName: string, errorType: string): ValidationErrors {
    const errors = group.controls[controlName].errors || {};
    errors[errorType] = true;
    group.controls[controlName].setErrors(errors);
    group.controls[controlName].markAsTouched();
    this.changeDetectorRef.markForCheck();
    return isEmpty(errors) ? undefined : errors;
  }

  private clearFormControlErrors(group: FormGroup, controlName: string, errorTypes: string[]): void {
    if (group.controls[controlName].errors) {
      const errors = group.controls[controlName].errors;
      for (const error of errorTypes) {
        delete errors[error];
      }
      group.controls[controlName].setErrors(isEmpty(errors) ? undefined : errors);
      this.changeDetectorRef.markForCheck();
    }
  }

With the above, I am expecting to, for example, have the startDate control be invalid after choosing a date that is beyond the endDate. Further, I expect the control to be in INVALID status, to be marked as touched and to have a relevant 'maxDate' error set in it's associated errors.

However, this is not what I am seeing. Surprisingly, my debugging efforts are yielding the following mystery that I am endeavoring to understand fully. Upon choosing a startDate beyond endDate as described above, I observe the following sequence in my debugging efforts: 1) this.form.get('startDate').valueChanges observer is invoked with the new value as expected 2) this.form.get('startDate').statusChanges observer reflects the following as expected startDate is now PENDING [null] startDate: 1582907100000 3) the cross field validation function invokes and sets the expected error on the startDate control 4) this.form.get('startDate').statusChanges observer fires again and reports startDate is now INVALID [{"maxDate":true}] startDate: 1582907100000 5) this.form.statusChanges observer fires and reports form is now INVALID [{"maxDate":true}]

All of the above appears to be as expected. However, I then also see the following immediately thereafter which I cannot explain: 6) this.form.get('startDate').statusChanges fires again and reflects startDate is now VALID [null] startDate: 1582907100000

Please note that there are no this.form.get('startDate').valueChanges observer changes noted between 5) and 6) nor are there any other programmatic changes to the control.

What am I missing? How can the control, which was correctly tagged as INVALID miraculously become VALID without any intervening value changes (nor any invocations to things such as group.controls[controlName].setErrors, clearFormControlErrors(...), reset, etc. that could plausibly be expected to reset that status to VALID?

Thank you in advance for your assistance!

Upvotes: 1

Views: 1923

Answers (1)

user3005818
user3005818

Reputation: 81

Just wanted to report back that, as expected, this mystery can be traced back to other extraneous elements and incorrect assumptions.

Indeed the value of the form controls are not being changed. However, other elements on the page triggered the changeDetector. Triggering this caused the validators, including the formgroup validator to rerun. Upon a subsequent rerun of the validator, the error messages are no longer applied to the individual controls (as they were in the first run) and only remain at the form/group level. This gave the false appearance (due to *ngIf on display of error messages that only expected the error on the control itself - not accounting for the form/group level as well) that the value had changed resulting in the validation status change.

Mystery resolved.

Upvotes: 0

Related Questions