From 975fbb3a78213d1cc6ad2355848ea93a91b06e23 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 24 Nov 2020 05:35:06 +0100 Subject: [PATCH] fix(material/datepicker): don't invoke change handler when filter is swapped out if result is the same (#20970) Doesn't invoke the `ControlValueAccessor` change function when a new date filter is assigned, if the result wouldn't have change the validation state. Fixes #20967. --- .../datepicker/date-range-input.spec.ts | 52 +++++++++++++++++++ src/material/datepicker/date-range-input.ts | 13 ++++- .../datepicker/datepicker-input-base.ts | 9 +++- src/material/datepicker/datepicker-input.ts | 6 ++- src/material/datepicker/datepicker.spec.ts | 42 ++++++++++++++- 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/material/datepicker/date-range-input.spec.ts b/src/material/datepicker/date-range-input.spec.ts index 29626d23ce7a..33b12f796137 100644 --- a/src/material/datepicker/date-range-input.spec.ts +++ b/src/material/datepicker/date-range-input.spec.ts @@ -20,6 +20,7 @@ import {BACKSPACE} from '@angular/cdk/keycodes'; import {MatDateRangeInput} from './date-range-input'; import {MatDateRangePicker} from './date-range-picker'; import {MatStartDate, MatEndDate} from './date-range-input-parts'; +import {Subscription} from 'rxjs'; describe('MatDateRangeInput', () => { function createComponent( @@ -317,6 +318,57 @@ describe('MatDateRangeInput', () => { expect(end.errors?.matDatepickerFilter).toBeTruthy(); }); + it('should should revalidate when a new date filter function is assigned', () => { + const fixture = createComponent(StandardRangePicker); + fixture.detectChanges(); + const {start, end} = fixture.componentInstance.range.controls; + const date = new Date(2020, 2, 2); + start.setValue(date); + end.setValue(date); + fixture.detectChanges(); + + const spy = jasmine.createSpy('change spy'); + const subscription = new Subscription(); + subscription.add(start.valueChanges.subscribe(spy)); + subscription.add(end.valueChanges.subscribe(spy)); + + fixture.componentInstance.dateFilter = () => false; + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(2); + + fixture.componentInstance.dateFilter = () => true; + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(4); + + subscription.unsubscribe(); + }); + + it('should not dispatch the change event if a new filter function with the same result ' + + 'is assigned', () => { + const fixture = createComponent(StandardRangePicker); + fixture.detectChanges(); + const {start, end} = fixture.componentInstance.range.controls; + const date = new Date(2020, 2, 2); + start.setValue(date); + end.setValue(date); + fixture.detectChanges(); + + const spy = jasmine.createSpy('change spy'); + const subscription = new Subscription(); + subscription.add(start.valueChanges.subscribe(spy)); + subscription.add(end.valueChanges.subscribe(spy)); + + fixture.componentInstance.dateFilter = () => false; + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(2); + + fixture.componentInstance.dateFilter = () => false; + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + }); + it('should throw if there is no start input', () => { expect(() => { const fixture = createComponent(RangePickerNoStart); diff --git a/src/material/datepicker/date-range-input.ts b/src/material/datepicker/date-range-input.ts index 2bd5080064d2..ad6bf690c905 100644 --- a/src/material/datepicker/date-range-input.ts +++ b/src/material/datepicker/date-range-input.ts @@ -122,8 +122,19 @@ export class MatDateRangeInput implements MatFormFieldControl>, @Input() get dateFilter() { return this._dateFilter; } set dateFilter(value: DateFilterFn) { + const start = this._startInput; + const end = this._endInput; + const wasMatchingStart = start && start._matchesFilter(start.value); + const wasMatchingEnd = end && end._matchesFilter(start.value); this._dateFilter = value; - this._revalidate(); + + if (start && start._matchesFilter(start.value) !== wasMatchingStart) { + start._validatorOnChange(); + } + + if (end && end._matchesFilter(end.value) !== wasMatchingEnd) { + end._validatorOnChange(); + } } private _dateFilter: DateFilterFn; diff --git a/src/material/datepicker/datepicker-input-base.ts b/src/material/datepicker/datepicker-input-base.ts index 21c85dc3e201..42760b79fb79 100644 --- a/src/material/datepicker/datepicker-input-base.ts +++ b/src/material/datepicker/datepicker-input-base.ts @@ -152,8 +152,7 @@ export abstract class MatDatepickerInputBase { const controlValue = this._dateAdapter.getValidDateOrNull( this._dateAdapter.deserialize(control.value)); - const dateFilter = this._getDateFilter(); - return !dateFilter || !controlValue || dateFilter(controlValue) ? + return !controlValue || this._matchesFilter(controlValue) ? null : {'matDatepickerFilter': true}; } @@ -392,6 +391,12 @@ export abstract class MatDatepickerInputBase` that // may accept different types. static ngAcceptInputType_value: any; diff --git a/src/material/datepicker/datepicker-input.ts b/src/material/datepicker/datepicker-input.ts index 4b0b3ebf98bb..666b54c5d815 100644 --- a/src/material/datepicker/datepicker-input.ts +++ b/src/material/datepicker/datepicker-input.ts @@ -112,8 +112,12 @@ export class MatDatepickerInput extends MatDatepickerInputBase @Input('matDatepickerFilter') get dateFilter() { return this._dateFilter; } set dateFilter(value: DateFilterFn) { + const wasMatchingValue = this._matchesFilter(this.value); this._dateFilter = value; - this._validatorOnChange(); + + if (this._matchesFilter(this.value) !== wasMatchingValue) { + this._validatorOnChange(); + } } private _dateFilter: DateFilterFn; diff --git a/src/material/datepicker/datepicker.spec.ts b/src/material/datepicker/datepicker.spec.ts index 2cc47ab1d716..a8787c84de33 100644 --- a/src/material/datepicker/datepicker.spec.ts +++ b/src/material/datepicker/datepicker.spec.ts @@ -1506,6 +1506,45 @@ describe('MatDatepicker', () => { expect(cells[0].classList).toContain('mat-calendar-body-disabled'); expect(cells[1].classList).not.toContain('mat-calendar-body-disabled'); }); + + it('should revalidate when a new function is assigned', fakeAsync(() => { + const classList = fixture.debugElement.query(By.css('input'))!.nativeElement.classList; + testComponent.date = new Date(2017, JAN, 1); + testComponent.filter = () => true; + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(classList).not.toContain('ng-invalid'); + + testComponent.filter = () => false; + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(classList).toContain('ng-invalid'); + })); + + it('should not dispatch the change event if a new function with the same result is assigned', + fakeAsync(() => { + const spy = jasmine.createSpy('change spy'); + const subscription = fixture.componentInstance.model.valueChanges?.subscribe(spy); + testComponent.filter = () => false; + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + + testComponent.filter = () => false; + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + subscription?.unsubscribe(); + })); + }); describe('datepicker with change and input events', () => { @@ -2342,8 +2381,9 @@ class DatepickerWithMinAndMaxValidation { }) class DatepickerWithFilterAndValidation { @ViewChild('d') datepicker: MatDatepicker; + @ViewChild(NgModel) model: NgModel; date: Date; - filter = (date: Date) => date.getDate() != 1; + filter = (date: Date | null) => date?.getDate() != 1; }