Skip to content

Commit

Permalink
fix(datepicker): emitting dateChange event when opposite range input …
Browse files Browse the repository at this point in the history
…changes (#19995)

Adds some logic to prevent date range inputs from emitting their `dateChange` and `dateInput` events when a value is assigned to the opposite input.

Fixes #19968.

(cherry picked from commit dc245d2)
  • Loading branch information
crisbeto authored and wagnermaciel committed Jul 21, 2020
1 parent 48a226b commit d383bba
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 4 deletions.
10 changes: 9 additions & 1 deletion src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import {BooleanInput} from '@angular/cdk/coercion';
import {BACKSPACE} from '@angular/cdk/keycodes';
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
import {DateRange} from './date-selection-model';
import {DateRange, DateSelectionModelChange} from './date-selection-model';

/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
export interface MatDateRangeInputParent<D> {
Expand Down Expand Up @@ -238,6 +238,10 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
}
}

protected _canEmitChangeEvent = (event: DateSelectionModelChange<DateRange<D>>): boolean => {
return event.source !== this._rangeInput._endInput;
}

protected _formatValue(value: D | null) {
super._formatValue(value);

Expand Down Expand Up @@ -318,6 +322,10 @@ export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdat
}
}

protected _canEmitChangeEvent = (event: DateSelectionModelChange<DateRange<D>>): boolean => {
return event.source !== this._rangeInput._startInput;
}

_onKeydown(event: KeyboardEvent) {
// If the user is pressing backspace on an empty end input, move focus back to the start.
if (event.keyCode === BACKSPACE && !this._elementRef.nativeElement.value) {
Expand Down
72 changes: 72 additions & 0 deletions src/material/datepicker/date-range-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {FocusMonitor} from '@angular/cdk/a11y';
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';

describe('MatDateRangeInput', () => {
function createComponent<T>(component: Type<T>): ComponentFixture<T> {
Expand Down Expand Up @@ -556,6 +557,75 @@ describe('MatDateRangeInput', () => {
subscription.unsubscribe();
});

it('should emit to the dateChange event only when typing in the relevant input', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
const {startInput, endInput, start, end} = fixture.componentInstance;
const startSpy = jasmine.createSpy('matStartDate spy');
const endSpy = jasmine.createSpy('matEndDate spy');
const startSubscription = startInput.dateChange.subscribe(startSpy);
const endSubscription = endInput.dateChange.subscribe(endSpy);

start.nativeElement.value = '10/10/2020';
dispatchFakeEvent(start.nativeElement, 'change');
fixture.detectChanges();

expect(startSpy).toHaveBeenCalledTimes(1);
expect(endSpy).not.toHaveBeenCalled();

start.nativeElement.value = '11/10/2020';
dispatchFakeEvent(start.nativeElement, 'change');
fixture.detectChanges();

expect(startSpy).toHaveBeenCalledTimes(2);
expect(endSpy).not.toHaveBeenCalled();

end.nativeElement.value = '11/10/2020';
dispatchFakeEvent(end.nativeElement, 'change');
fixture.detectChanges();

expect(startSpy).toHaveBeenCalledTimes(2);
expect(endSpy).toHaveBeenCalledTimes(1);

end.nativeElement.value = '12/10/2020';
dispatchFakeEvent(end.nativeElement, 'change');
fixture.detectChanges();

expect(startSpy).toHaveBeenCalledTimes(2);
expect(endSpy).toHaveBeenCalledTimes(2);

startSubscription.unsubscribe();
endSubscription.unsubscribe();
});

it('should emit to the dateChange event when setting the value programmatically', () => {
const fixture = createComponent(StandardRangePicker);
fixture.detectChanges();
const {startInput, endInput} = fixture.componentInstance;
const {start, end} = fixture.componentInstance.range.controls;
const startSpy = jasmine.createSpy('matStartDate spy');
const endSpy = jasmine.createSpy('matEndDate spy');
const startSubscription = startInput.dateChange.subscribe(startSpy);
const endSubscription = endInput.dateChange.subscribe(endSpy);

start.setValue(new Date(2020, 1, 2));
end.setValue(new Date(2020, 2, 2));
fixture.detectChanges();

expect(startSpy).not.toHaveBeenCalled();
expect(endSpy).not.toHaveBeenCalled();

start.setValue(new Date(2020, 3, 2));
end.setValue(new Date(2020, 4, 2));
fixture.detectChanges();

expect(startSpy).not.toHaveBeenCalled();
expect(endSpy).not.toHaveBeenCalled();

startSubscription.unsubscribe();
endSubscription.unsubscribe();
});

});

@Component({
Expand Down Expand Up @@ -585,6 +655,8 @@ describe('MatDateRangeInput', () => {
class StandardRangePicker {
@ViewChild('start') start: ElementRef<HTMLInputElement>;
@ViewChild('end') end: ElementRef<HTMLInputElement>;
@ViewChild(MatStartDate) startInput: MatStartDate<Date>;
@ViewChild(MatEndDate) endInput: MatEndDate<Date>;
@ViewChild(MatDateRangeInput) rangeInput: MatDateRangeInput<Date>;
@ViewChild(MatDateRangePicker) rangePicker: MatDateRangePicker<Date>;
separator = '–';
Expand Down
19 changes: 16 additions & 3 deletions src/material/datepicker/datepicker-input-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import {
} from '@angular/material/core';
import {Subscription} from 'rxjs';
import {createMissingDateImplError} from './datepicker-errors';
import {ExtractDateTypeFromSelection, MatDateSelectionModel} from './date-selection-model';
import {
ExtractDateTypeFromSelection,
MatDateSelectionModel,
DateSelectionModelChange,
} from './date-selection-model';

/**
* An event used for datepicker input and change events. We don't always have access to a native
Expand Down Expand Up @@ -198,8 +202,14 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
this._cvaOnChange(value);
this._onTouched();
this._formatValue(value);
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));

// Note that we can't wrap the entire block with this logic, because for the range inputs
// we want to revalidate whenever either one of the inputs changes and we don't have a
// good way of distinguishing it at the moment.
if (this._canEmitChangeEvent(event)) {
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
}

if (this._outsideValueChanged) {
this._outsideValueChanged();
Expand All @@ -226,6 +236,9 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
*/
protected abstract _outsideValueChanged?: () => void;

/** Predicate that determines whether we're allowed to emit a particular change event. */
protected abstract _canEmitChangeEvent(event: DateSelectionModelChange<S>): boolean;

/** Whether the last value set on the input was valid. */
protected _lastValueValid = false;

Expand Down
4 changes: 4 additions & 0 deletions src/material/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
return this._dateFilter;
}

protected _canEmitChangeEvent() {
return true;
}

// Unnecessary when selecting a single date.
protected _outsideValueChanged: undefined;

Expand Down
3 changes: 3 additions & 0 deletions tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | nu
set min(value: D | null);
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField: MatFormField);
protected _assignValueToModel(value: D | null): void;
protected _canEmitChangeEvent(): boolean;
protected _getDateFilter(): DateFilterFn<D | null>;
_getMaxDate(): D | null;
_getMinDate(): D | null;
Expand Down Expand Up @@ -364,6 +365,7 @@ export declare abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSe
}

export declare class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
protected _canEmitChangeEvent: (event: DateSelectionModelChange<DateRange<D>>) => boolean;
protected _validator: ValidatorFn | null;
constructor(rangeInput: MatDateRangeInputParent<D>, elementRef: ElementRef<HTMLInputElement>, defaultErrorStateMatcher: ErrorStateMatcher, injector: Injector, parentForm: NgForm, parentFormGroup: FormGroupDirective, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats);
protected _assignValueToModel(value: D | null): void;
Expand Down Expand Up @@ -468,6 +470,7 @@ export declare class MatSingleDateSelectionModel<D> extends MatDateSelectionMode
}

export declare class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
protected _canEmitChangeEvent: (event: DateSelectionModelChange<DateRange<D>>) => boolean;
protected _validator: ValidatorFn | null;
constructor(rangeInput: MatDateRangeInputParent<D>, elementRef: ElementRef<HTMLInputElement>, defaultErrorStateMatcher: ErrorStateMatcher, injector: Injector, parentForm: NgForm, parentFormGroup: FormGroupDirective, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats);
protected _assignValueToModel(value: D | null): void;
Expand Down

0 comments on commit d383bba

Please sign in to comment.