angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.39k stars 6.76k 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()

Hapanmuffinssi commented 1 month ago

Any update on this? Just banged head to the desk for few days wondering why all cross validation at formgroup level seemed to fail between two custom datetime picker controls. At formgroup level where the custom controls where used as a part of a large form all controls seemed to be valid and nothing had errors present. Still, the whole form was seen as invalid until some parts of the custom controls (either date or time subcontrols) were modified directly via keyboard input.

Custom control in this case is a datetime-picker which uses mat-datepicker for the date part and a time dropdown-input field for the time part. Then the values are combined as the value for the custom control. We made date validator functions which compare values of these custom controls. They are used to mark the beginning and the end of an event for example.

Angular & Angular Material are currently at version 18.2.9 in our project.

Fortunately our dynamic form composer has an intermediate layer which constructs the final form for the UI. We managed to pass the inner custom control form instances back to this layer and then, similarly to the directives above, we were able to listen to value changes of the inner controls inside the custom controls and revalidate the needed parts in the "opposing" custom controls' inner controls. Only then the main form could be validated successfully.

Thanks for the workaround idea. Time selection would be a nice addition to the official mat-datepicker component :) Would make life easier in many projects.

EDIT: Same story with two simple custom controls with only time input + dropdown for time selection. Let's say timepicker A must have a value that's less than the value of timepicker B. Comparison is made with validator functions. If A is set to, let's say 13:00 and B 14:00, this is valid. Then B is set to 11:00 which is marked as invalid. Then B is fixed again as 14:00 which again is marked as valid. So far so good.

Now, let's set A to 15:00 which is marked properly as invalid. Then B is set as 16:00 which should remove error from A but nothing happens. But just following the opposite field's value updates and then updating the value and validity of the field itself, fixes the situation instantly. Maybe the reactive forms could have some kind of cross-validation parameters built-in for a form control? This could be really nasty to fix otherwise.