0

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
    )
  );
}

1 Answer 1

1

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.

Demo @ StackBlitz

.container {
  display: block;
  height: 150px;
  overflow: visible;
}
Sign up to request clarification or add additional context in comments.

2 Comments

This is a nice alternative. I just figured this out, via the normal angular way. that gives fine-grained control. Attach a validator to the formArray, and put the validation logic in there (checking the current and next formGroup). In the validator set the errors to the 2 relevant formGroups (and controls inside them). That way, only the relevant formGroups and controls inside them display the error. Errors are attached to formGroup to be more reactive - but attaching them to just the controls works too.
@sachin, in your another question you have another way to do it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.