When our code becomes tangled, it's time to take a step back and see if we can simplify. There are too many variables in the code: datesArray, datesInfo, formGetters, startDateAfterEndDateMatchers,... all interconnected
But really, we only need one: datesInfo, and as usual, we use a FormArray and a getter of the formArray
protected datesInfo: FormGroup = this.formBuilder.group({});
get datesArray()
{
return this.datesInfo.get('datesArray') as FormArray
}
We'll iterate through datesArray.controls and utilize datesInfo.get(path) and datesInfo.hasError('error',path) to access the controls.
With a FormArray, the path could look like datesArray.0.startDate
for the startDate of the first FormGroup, datesArray.1.startDate
for the second one, and so on...
<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 default, 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 or before previous 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>
Regarding matchError, I propose a different approach: assigning the error to the FormControl instead of the FormGroup of the formArray. The only challenge with this approach is that we also need to validate the formControl when another formControl changes: we must check endDate not just when it changes but also when startDate changes.
To achieve this, 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 initialize the formGroup like this:
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')],
},
],
},
)
)
),
});
}
Check out stackblitz