angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.33k stars 6.73k forks source link

bug(datepicker): Date range picker incorrectly has matStartDateInvalid error after modification #21875

Open j-martinez-dev opened 3 years ago

j-martinez-dev commented 3 years ago

Reproduction

Use StackBlitz to reproduce your issue: https://stackblitz.com/edit/angular-uotl3m-yzo7qt

Steps to reproduce:

  1. Pick a date range in date picker.
  2. Modify the start date using the input with a date after the end date
  3. Modify the end date using the input with a date after the start date

    Expected Behavior

The FormGroup is valid and not error message is show.

Actual Behavior

What behavior did you actually see?

The error message "Invalid start date" is show.

If you change the version of material from "^11.0.0" to "11.0.0" there is no problemr

Environment

crisbeto commented 3 years ago

Definitely an issue, but I suspect that it'll be difficult to resolve without regressing on other issues like https://github.com/angular/components/issues/20213.

marek-aguita commented 3 years ago

This directive could serve as a hot fix until this is fixed in the date range picker component itself:

@Directive({
  selector: 'mat-date-range-input'
})
export class UpdateDateRangeValueAndValidityFixerDirective
  implements AfterContentInit, OnDestroy {
  private readonly destroyed$ = new Subject<void>();

  constructor(private readonly formGroupName: FormGroupName) {}

  ngAfterContentInit(): void {
    const startFormControl = this.formGroupName.control.get('start'); //unfortunatelly this would work only if the form controls are named 'start' or 'end', feel free to use your names. I've used the ones from the showcase that is in the official docs https://material.angular.io/components/datepicker/examples
    const endFormControl = this.formGroupName.control.get('end');

    startFormControl.valueChanges
      .pipe(distinctUntilChanged(), takeUntil(this.destroyed$))
      .subscribe(() => endFormControl.updateValueAndValidity());

    endFormControl.valueChanges
      .pipe(distinctUntilChanged(), takeUntil(this.destroyed$))
      .subscribe(() => startFormControl.updateValueAndValidity());
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
  }
}
dwilches commented 3 years ago

Found this issue today. Happening as of Angular 12.1.1

mateusduraes commented 3 years ago

I'm also facing the same issue using the following versions

"@angular/common": "12.1.3",
"@angular/material": "12.1.3",

Are there any plans for this issue to be fixed?

mibnd commented 2 years ago

@marek-aguita , great solution. I improve a little your directive to not depends on form control names.

@Directive({
  selector: 'mat-date-range-input'
})
export class UpdateDateRangeValueAndValidityFixerDirective
  implements AfterContentInit, OnDestroy {
+  @ContentChild(MatStartDate, { read: FormControlName }) startDateControlName: FormControlName;
+  @ContentChild(MatEndDate, { read: FormControlName }) endDateControlName: FormControlName;
+
  private readonly destroyed$ = new Subject<void>();

-  constructor(private readonly formGroupName: FormGroupName) {}
-
  ngAfterContentInit(): void {
-    const startFormControl = this.formGroupName.control.get('start'); //unfortunatelly this would work only if the form controls are named 'start' or 'end', feel free to use your names. I've used the ones from the showcase that is in the official docs https://material.angular.io/components/datepicker/examples
-    const endFormControl = this.formGroupName.control.get('end');
-
-    startFormControl.valueChanges
+    this.startDateControlName.valueChanges
      .pipe(distinctUntilChanged(), takeUntil(this.destroyed$))
-      .subscribe(() => endFormControl.updateValueAndValidity());
+      .subscribe(() => setTimeout(() => this.endDateControlName.control.updateValueAndValidity()));

-    endFormControl.valueChanges
+    this.endDateControlName.valueChanges
      .pipe(distinctUntilChanged(), takeUntil(this.destroyed$))
-      .subscribe(() => startFormControl.updateValueAndValidity());
+      .subscribe(() => setTimeout(() => this.startDateControlName.control.updateValueAndValidity()));
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
  }
}
Whole directive ```typescript @Directive({ selector: 'mat-date-range-input', }) export class UpdateDateRangeValueAndValidityFixerDirective implements AfterContentInit, OnDestroy { @ContentChild(MatStartDate, { read: FormControlName }) startDateControlName: FormControlName; @ContentChild(MatEndDate, { read: FormControlName }) endDateControlName: FormControlName; private readonly destroyed$ = new Subject(); ngAfterContentInit(): void { this.startDateControlName.valueChanges .pipe(distinctUntilChanged(), takeUntil(this.destroyed$)) .subscribe(() => setTimeout(() => this.endDateControlName.control.updateValueAndValidity())); this.endDateControlName.valueChanges .pipe(distinctUntilChanged(), takeUntil(this.destroyed$)) .subscribe(() => setTimeout(() => this.startDateControlName.control.updateValueAndValidity())); } ngOnDestroy(): void { this.destroyed$.next(); } } ```

UPD 2023-04-03: Add setTimeout()