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!