When our code is confussed, we need take a breath and check if we can factorize. The code has too much variables:datesArray, datesInfo, formGetters, startDateAfterEndDateMatchers,... realationated
And we only need one: datesInfo and, as always we use a FormArray a getter of the formArray
protected datesInfo: FormGroup = this.formBuilder.group({});
get datesArray()
{
return this.datesInfo.get('datesArray') as FormArray
}
We are going to loop over datesArray.controls and we are going to use datesInfo.get(path) and datesInfo.hasError('error',path) to reach the controls.
The path, when we have an FormArray can be in the way datesArray.0.startDate for the startDate of the first FormGroup of the form, datesArray.1.startDate for the second one, etc...
<form *ngIf="datesInfo.get('datesArray')" [formGroup]="datesInfo" class="form-group">
<div formArrayName="datesArray">
@for(group of datesArray.controls;track $index)
{
<!--we indicate the formGroup-->
<div [formGroupName]="$index">
<mat-form-field class="form-date">
<mat-label>
Start Date
</mat-label>
<!--we use formControlName, not FormControl-->
<input
matInput id="startDate-{{$index}}"
[matDatepicker]="startDatePicker"
formControlName="startDate"
autocomplete="off"
required/>
<mat-hint>DD/MM/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="startDatePicker" [disabled]="false">
</mat-datepicker-toggle>
<!--see the use of get('datesArray.'+($index-1)+'.endDate')-->
<mat-datepicker #startDatePicker
[startAt]="$index?datesInfo.get('datesArray.'+($index-1)+'.endDate')?.value:null">
</mat-datepicker>
<!-- a mat-error, by defect, only show if touched, so
we only check the "type of error"
-->
<mat-error
*ngIf="datesInfo.hasError('required','datesArray.'+$index+'.startDate')">
Start Date is required.
</mat-error>
<mat-error
*ngIf="datesInfo.hasError('lessDate','datesArray.'+$index+'.startDate')">
Cannot be before the end Date of before row
</mat-error>
</mat-form-field>
<mat-form-field class="form-date">
<mat-label>
End Date
</mat-label>
<input
(keydown)="endDatePicker.open()"
(click)="endDatePicker.open()"
matInput id="endDate-{{$index}}"
[matDatepicker]="endDatePicker"
formControlName="endDate"
autocomplete="off"/>
<mat-hint>DD/MM/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="endDatePicker" [disabled]="false">
</mat-datepicker-toggle>
<mat-datepicker #endDatePicker
[startAt]="datesInfo.get('datesArray.'+$index+'.startDate')?.value">
</mat-datepicker>
<mat-error
*ngIf="datesInfo.hasError('required','datesArray.'+$index+'.endDate')">
End Date is required.
</mat-error>
<mat-error
*ngIf="datesInfo.hasError('lessDate','datesArray.'+$index+'.endDate')">
Cannot be before Start Date
</mat-error>
</mat-form-field>
</div>
}
</div>
</form>
About matchError. I suggest another aproach: makes the error belong to the FormControl, not to the FormGroup of the formArray. The only problem with this aproach it's that we need validate also the formControl when another formControl Change: we need check endDate, not only when change the endDate else also when change the startDate.
For this we are going to define a Validator that return always null, but check a formControl. It's looks like this SO
We define two functions like:
greaterThan(dateCompare:string)
{
return (control:AbstractControl)=>{
if (!control.value)
return null;
const group=control.parent as FormGroup;
const formArray=group?group.parent as FormArray:null;
if (group && formArray)
{
const index=dateCompare=='startDate'? formArray.controls.findIndex(x=>x==group):formArray.controls.findIndex(x=>x==group)-1;
if (index>=0)
{
const date=formArray.at(index).get(dateCompare)?.value
if (date && control.value && control.value.getTime()<date.getTime())
return {lessDate:true}
}
}
return null
}
}
checkAlso(dateCheck:string){
return (control:AbstractControl)=>{
const group=control.parent as FormGroup;
const formArray=group?group.parent as FormArray:null;
if (group && formArray)
{
const index=dateCheck=='endDate'? formArray.controls.findIndex(x=>x==group):formArray.controls.findIndex(x=>x==group)+1;
if (index>=0 && index<formArray.controls.length)
{
const control=formArray.at(index).get(dateCheck)
control && control.updateValueAndValidity()
}
}
return null
}
And we create the formGroup as
private initFormGroup() {
this.datesInfo = this.formBuilder.group({
datesArray: this.formBuilder.array(
([1,2,3]).map((_) =>
this.formBuilder.group(
{
startDate: [
'',
{
nonNullable: true,
validators: [Validators.required,this.greaterThan("endDate"),this.checkAlso('endDate')],
},
],
endDate: [
'',
{
validators: [this.greaterThan("startDate"),this.checkAlso('startDate')],
},
],
},
)
)
),
});
}
stackblitz