This works on a simple form, with a single start and end date, but now the form I have is dynamic which has multiple pairs of start and end dates, so I've had to use a FormArray
.
Here is the structure, but now I can't get the error state matchers, or the validation on each FormGroup (inside the FormArray) to work
/* error state matchers */
readonly startDateAfterEndDateMatchers: SingleErrorStateMatcher[] = [];
/* lifecycle hooks */
protected ngOnInit(): void {
// initialisation
this.initFormGroup();
this.formGetters = this.initFormGetters(this.datesInfo);
// create error state matchers
for (let i = 0; i < this.datesArray.length; i++) {
this.startDateAfterEndDateMatchers.push(
new SingleErrorStateMatcher('startDateAfterEndDate')
);
}
}
// INITIALISE FORM
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] }
)
)
),
});
}
Here is the Stackblitz too (it uses Angular Material components) Any help would be appreciated: https://stackblitz.com/edit/stackblitz-starters-ss9qeg?file=src%2Fmain.ts
Thanks in advance.
ST
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')],
},
],
},
)
)
),
});
}