Skip to content

Commit ee78609

Browse files
crisbetoannieyw
authored andcommitted
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. (cherry picked from commit 975fbb3)
1 parent b0cee6e commit ee78609

File tree

5 files changed

+117
-5
lines changed

5 files changed

+117
-5
lines changed

src/material/datepicker/date-range-input.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {BACKSPACE} from '@angular/cdk/keycodes';
2020
import {MatDateRangeInput} from './date-range-input';
2121
import {MatDateRangePicker} from './date-range-picker';
2222
import {MatStartDate, MatEndDate} from './date-range-input-parts';
23+
import {Subscription} from 'rxjs';
2324

2425
describe('MatDateRangeInput', () => {
2526
function createComponent<T>(
@@ -317,6 +318,57 @@ describe('MatDateRangeInput', () => {
317318
expect(end.errors?.matDatepickerFilter).toBeTruthy();
318319
});
319320

321+
it('should should revalidate when a new date filter function is assigned', () => {
322+
const fixture = createComponent(StandardRangePicker);
323+
fixture.detectChanges();
324+
const {start, end} = fixture.componentInstance.range.controls;
325+
const date = new Date(2020, 2, 2);
326+
start.setValue(date);
327+
end.setValue(date);
328+
fixture.detectChanges();
329+
330+
const spy = jasmine.createSpy('change spy');
331+
const subscription = new Subscription();
332+
subscription.add(start.valueChanges.subscribe(spy));
333+
subscription.add(end.valueChanges.subscribe(spy));
334+
335+
fixture.componentInstance.dateFilter = () => false;
336+
fixture.detectChanges();
337+
expect(spy).toHaveBeenCalledTimes(2);
338+
339+
fixture.componentInstance.dateFilter = () => true;
340+
fixture.detectChanges();
341+
expect(spy).toHaveBeenCalledTimes(4);
342+
343+
subscription.unsubscribe();
344+
});
345+
346+
it('should not dispatch the change event if a new filter function with the same result ' +
347+
'is assigned', () => {
348+
const fixture = createComponent(StandardRangePicker);
349+
fixture.detectChanges();
350+
const {start, end} = fixture.componentInstance.range.controls;
351+
const date = new Date(2020, 2, 2);
352+
start.setValue(date);
353+
end.setValue(date);
354+
fixture.detectChanges();
355+
356+
const spy = jasmine.createSpy('change spy');
357+
const subscription = new Subscription();
358+
subscription.add(start.valueChanges.subscribe(spy));
359+
subscription.add(end.valueChanges.subscribe(spy));
360+
361+
fixture.componentInstance.dateFilter = () => false;
362+
fixture.detectChanges();
363+
expect(spy).toHaveBeenCalledTimes(2);
364+
365+
fixture.componentInstance.dateFilter = () => false;
366+
fixture.detectChanges();
367+
expect(spy).toHaveBeenCalledTimes(2);
368+
369+
subscription.unsubscribe();
370+
});
371+
320372
it('should throw if there is no start input', () => {
321373
expect(() => {
322374
const fixture = createComponent(RangePickerNoStart);

src/material/datepicker/date-range-input.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,19 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
122122
@Input()
123123
get dateFilter() { return this._dateFilter; }
124124
set dateFilter(value: DateFilterFn<D>) {
125+
const start = this._startInput;
126+
const end = this._endInput;
127+
const wasMatchingStart = start && start._matchesFilter(start.value);
128+
const wasMatchingEnd = end && end._matchesFilter(start.value);
125129
this._dateFilter = value;
126-
this._revalidate();
130+
131+
if (start && start._matchesFilter(start.value) !== wasMatchingStart) {
132+
start._validatorOnChange();
133+
}
134+
135+
if (end && end._matchesFilter(end.value) !== wasMatchingEnd) {
136+
end._validatorOnChange();
137+
}
127138
}
128139
private _dateFilter: DateFilterFn<D>;
129140

src/material/datepicker/datepicker-input-base.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
152152
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
153153
const controlValue = this._dateAdapter.getValidDateOrNull(
154154
this._dateAdapter.deserialize(control.value));
155-
const dateFilter = this._getDateFilter();
156-
return !dateFilter || !controlValue || dateFilter(controlValue) ?
155+
return !controlValue || this._matchesFilter(controlValue) ?
157156
null : {'matDatepickerFilter': true};
158157
}
159158

@@ -392,6 +391,12 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
392391
return false;
393392
}
394393

394+
/** Gets whether a value matches the current date filter. */
395+
_matchesFilter(value: D | null): boolean {
396+
const filter = this._getDateFilter();
397+
return !filter || filter(value);
398+
}
399+
395400
// Accept `any` to avoid conflicts with other directives on `<input>` that
396401
// may accept different types.
397402
static ngAcceptInputType_value: any;

src/material/datepicker/datepicker-input.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,12 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
113113
@Input('matDatepickerFilter')
114114
get dateFilter() { return this._dateFilter; }
115115
set dateFilter(value: DateFilterFn<D | null>) {
116+
const wasMatchingValue = this._matchesFilter(this.value);
116117
this._dateFilter = value;
117-
this._validatorOnChange();
118+
119+
if (this._matchesFilter(this.value) !== wasMatchingValue) {
120+
this._validatorOnChange();
121+
}
118122
}
119123
private _dateFilter: DateFilterFn<D | null>;
120124

src/material/datepicker/datepicker.spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,45 @@ describe('MatDatepicker', () => {
14901490
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
14911491
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
14921492
});
1493+
1494+
it('should revalidate when a new function is assigned', fakeAsync(() => {
1495+
const classList = fixture.debugElement.query(By.css('input'))!.nativeElement.classList;
1496+
testComponent.date = new Date(2017, JAN, 1);
1497+
testComponent.filter = () => true;
1498+
fixture.detectChanges();
1499+
flush();
1500+
fixture.detectChanges();
1501+
1502+
expect(classList).not.toContain('ng-invalid');
1503+
1504+
testComponent.filter = () => false;
1505+
fixture.detectChanges();
1506+
flush();
1507+
fixture.detectChanges();
1508+
1509+
expect(classList).toContain('ng-invalid');
1510+
}));
1511+
1512+
it('should not dispatch the change event if a new function with the same result is assigned',
1513+
fakeAsync(() => {
1514+
const spy = jasmine.createSpy('change spy');
1515+
const subscription = fixture.componentInstance.model.valueChanges?.subscribe(spy);
1516+
testComponent.filter = () => false;
1517+
fixture.detectChanges();
1518+
flush();
1519+
fixture.detectChanges();
1520+
1521+
expect(spy).toHaveBeenCalledTimes(1);
1522+
1523+
testComponent.filter = () => false;
1524+
fixture.detectChanges();
1525+
flush();
1526+
fixture.detectChanges();
1527+
1528+
expect(spy).toHaveBeenCalledTimes(1);
1529+
subscription?.unsubscribe();
1530+
}));
1531+
14931532
});
14941533

14951534
describe('datepicker with change and input events', () => {
@@ -2325,8 +2364,9 @@ class DatepickerWithMinAndMaxValidation {
23252364
})
23262365
class DatepickerWithFilterAndValidation {
23272366
@ViewChild('d') datepicker: MatDatepicker<Date>;
2367+
@ViewChild(NgModel) model: NgModel;
23282368
date: Date;
2329-
filter = (date: Date) => date.getDate() != 1;
2369+
filter = (date: Date | null) => date?.getDate() != 1;
23302370
}
23312371

23322372

0 commit comments

Comments
 (0)