the below code works,
https://stackblitz.com/edit/stackblitz-starters-wnququ?file=src%2Fmain.html
But I need to take it a step further and ensure that the end date of the FormGroup
at index (x) is not greater than or equal to the start date of FormGroup
at index (x + 1) - (all inside the main form array).
Do you know how I do that?
This is what I have so far (refer to Stackblitz demo too)
Validators
Currently, my date validator looks like:
// VALIDATORS
public startDateAfterEndDateMatcher: ValidatorFn =
this.dateComparisonValidator(
'startDate',
'endDate',
'startDateAfterEndDate',
(date1: Date, date2: Date) => date1 && date2 && date1 > date2
);
private dateComparisonValidator(
fieldName1: string,
fieldName2: string,
errorName: string,
condition: (value1: any, value2: any) => boolean
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const field1Value = control.get(fieldName1)?.value;
const field2Value = control.get(fieldName2)?.value;
console.log('condition', condition(field1Value, field2Value));
if (condition(field1Value, field2Value)) {
const errors: ValidationErrors = {};
errors[errorName] = true;
return errors;
}
return null;
};
}
Form Structure
The form structure currently looks like this. The form validator gets added on to each formGroup object, (but I'd like to try and validate across formGroups now - which I'm not sure how to do)
private initFormGroup() {
this.datesInfo = this.formBuilder.group({
datesArray: this.formBuilder.array(
(this.datesArray || []).map((_) =>
this.formBuilder.group(
{
startDate: [
'',
{
nonNullable: true,
validators: [Validators.required],
},
],
endDate: [
'',
{
validators: [],
},
],
},
{ validators: [this.startDateAfterEndDateMatcher] }
)
)
),
});
}
Error State Matcher
My error state matcher (that attaches to each form group in the form array) looks like:
// ERROR MATCHER
export class SingleErrorStateMatcher implements ErrorStateMatcher {
private errorCode: string;
public constructor(errorCode: string, private formGroup?: FormGroup) {
this.errorCode = errorCode;
}
isErrorState(
control: FormControl | null,
formGroup: FormGroupDirective | NgForm | null
): boolean {
let parentFormGroup = this.formGroup ?? formGroup;
console.log('parentFormGroup', parentFormGroup);
return (
!!(parentFormGroup?.dirty || parentFormGroup?.touched) &&
!!(parentFormGroup?.invalid && parentFormGroup?.hasError(this.errorCode))
);
}
}
Initialisation
These get pushed inside ngOnInit
only (so it's not fully dynamic in the sense, I haven't yet thought about what happens, if I want to add another pair of dates- or if I delete/roll back a pair of dates... - but that's ok for now)
// create error state matchers
for (let i = 0; i < this.datesArray.length; i++) {
this.startDateAfterEndDateMatchers.push(
new SingleErrorStateMatcher(
'startDateAfterEndDate',
this.datesInfo.controls['datesArray'].get(`${i}`) as FormGroup
)
);
}
First, would like to clarify that ErrorStateMatcher
is used on how/when the <mat-error>
is displayed. So you shouldn't mix it with the validation logic.
Adjust the SingleErrorStateMatcher
to display the error when the FormGroup
is invalid instead of specified error (code).
export class SingleErrorStateMatcher implements ErrorStateMatcher {
private errorCode: string;
public constructor(errorCode: string, private formGroup?: FormGroup) {
this.errorCode = errorCode;
}
isErrorState(
control: FormControl | null,
formGroup: FormGroupDirective | NgForm | null
): boolean {
let parentFormGroup = this.formGroup ?? formGroup;
//console.log('parentFormGroup', parentFormGroup);
return (
!!(parentFormGroup?.dirty || parentFormGroup?.touched) &&
!!parentFormGroup?.invalid
);
}
}
For comparing the field with the consecutive (next) field, thinking that by subscribing to the dateArray
FormArray
's valuesChanges
observable and adding the validation will be easier.
subscription!: Subscription;
this.subscription = (
this.datesInfo.controls['datesArray'] as FormArray
).valueChanges.subscribe((dates) => {
dates.forEach((x: any, i: number) => {
const endDateExceedsStartDate = dates.some(
(y: any, j: number) =>
j == i + 1 && x.endDate && y.startDate && x.endDate >= y.startDate
);
const endDate = (
this.datesInfo.controls['datesArray']?.get(`${i}`) as FormGroup
)?.get('endDate')!;
if (endDateExceedsStartDate) {
endDate.setErrors(
{ endDateExceedsStartDate: true },
{ emitEvent: false }
);
} else {
if (endDate.hasError('endDateExceedsStartDate')) {
delete endDate.errors?.['endDateExceedsStartDate'];
endDate.updateValueAndValidity({ emitEvent: false });
}
}
});
});
And don't forget to unsubscribe the Subscription for the performance optimization.
ngOnDestroy() {
this.subscription.unsubscribe();
}
Showing the "endDateExceedsStartDate" error.
<mat-error
*ngIf="datesInfo.get('datesArray')!.get([$index])?.get('endDate')?.hasError('endDateExceedsStartDate')"
>
End Date cannot exceed (next) Start Date
</mat-error>
In case the <mat-error>
is not shown, it could be due to not enough space. Hence you may need to adjust the <mat-form-field>
/container to display the error message completely.
.container {
display: block;
height: 150px;
overflow: visible;
}