From dba2b6862b81d7b07668a61a18af299cab493c84 Mon Sep 17 00:00:00 2001 From: mmalerba Date: Tue, 11 Apr 2017 13:49:50 -0700 Subject: [PATCH] fix(datepicker): calendar should update when input changes (#3824) * fix(datepicker): calendar selected date should change when input changes * startAt fix * fix tests * address comments * fix unsubscribe * make valueChange on datepicker-input internal --- src/lib/core/datetime/simple-date.ts | 7 ++++++ src/lib/datepicker/calendar.ts | 2 +- src/lib/datepicker/datepicker-input.ts | 30 ++++++++++++++++++++------ src/lib/datepicker/datepicker.html | 3 ++- src/lib/datepicker/datepicker.spec.ts | 12 +++++------ src/lib/datepicker/datepicker.ts | 26 ++++++++++++++++------ 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/lib/core/datetime/simple-date.ts b/src/lib/core/datetime/simple-date.ts index 6546cd923ea2..365b59d28f9e 100644 --- a/src/lib/core/datetime/simple-date.ts +++ b/src/lib/core/datetime/simple-date.ts @@ -16,6 +16,13 @@ export class SimpleDate { return SimpleDate.fromNativeDate(new Date()); } + /** + * Checks whether the given dates are equal. Null dates are considered equal to other null dates. + */ + static equals(first: SimpleDate, second: SimpleDate): boolean { + return first && second ? !first.compare(second) : first == second; + } + /** The native JS Date. */ private _date: Date; diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 048d7c227c55..31244121cf19 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -113,7 +113,7 @@ export class MdCalendar implements AfterContentInit { /** Handles date selection in the month view. */ _dateSelected(date: SimpleDate): void { - if ((!date || !this.selected) && date != this.selected || date.compare(this.selected)) { + if (!SimpleDate.equals(date, this.selected)) { this.selectedChange.emit(date); } } diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index 9f6cc473db33..1bde8857b881 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -2,6 +2,7 @@ import { AfterContentInit, Directive, ElementRef, + EventEmitter, forwardRef, Input, OnDestroy, @@ -15,6 +16,7 @@ import {CalendarLocale} from '../core/datetime/calendar-locale'; import {Subscription} from 'rxjs/Subscription'; import {MdInputContainer} from '../input/input-container'; import {DOWN_ARROW} from '../core/keyboard/keycodes'; +import {Observable} from 'rxjs/Observable'; export const MD_DATEPICKER_VALUE_ACCESSOR: any = { @@ -34,7 +36,7 @@ export const MD_DATEPICKER_VALUE_ACCESSOR: any = { '[attr.aria-owns]': '_datepicker?.id', '[min]': '_min?.toNativeDate()', '[max]': '_max?.toNativeDate()', - '(input)': '_onChange($event.target.value)', + '(input)': '_onInput($event.target.value)', '(blur)': '_onTouched()', '(keydown)': '_onKeydown($event)', } @@ -56,14 +58,17 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor /** The value of the input. */ @Input() get value(): SimpleDate { - return this._value; + return this._locale.parseDate(this._elementRef.nativeElement.value); } set value(value: SimpleDate) { - this._value = this._locale.parseDate(value); - const stringValue = this._value == null ? '' : this._locale.formatDate(this._value); - this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', stringValue); + let date = this._locale.parseDate(value); + let oldDate = this.value; + this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', + date ? this._locale.formatDate(date) : ''); + if (!SimpleDate.equals(oldDate, date)) { + this._valueChangeEmitter.emit(date); + } } - private _value: SimpleDate; /** The minimum valid date. */ @Input() @@ -77,6 +82,11 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor set max(value: SimpleDate) { this._max = this._locale.parseDate(value); } private _max: SimpleDate; + private _valueChangeEmitter = new EventEmitter(); + + /** Emits when the value changes (either due to user input or programmatic change). */ + _valueChange: Observable = this._valueChangeEmitter.asObservable(); + _onChange = (value: any) => {}; _onTouched = () => {}; @@ -120,7 +130,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor // Implemented as part of ControlValueAccessor registerOnChange(fn: (value: any) => void): void { - this._onChange = value => fn(this._locale.parseDate(value)); + this._onChange = fn; } // Implemented as part of ControlValueAccessor @@ -139,4 +149,10 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAccessor event.preventDefault(); } } + + _onInput(value: string) { + let date = this._locale.parseDate(value); + this._onChange(date); + this._valueChangeEmitter.emit(date); + } } diff --git a/src/lib/datepicker/datepicker.html b/src/lib/datepicker/datepicker.html index 68c4abcaf2a0..ef816fde849a 100644 --- a/src/lib/datepicker/datepicker.html +++ b/src/lib/datepicker/datepicker.html @@ -7,6 +7,7 @@ [minDate]="_minDate" [maxDate]="_maxDate" [dateFilter]="dateFilter" - [(selected)]="_selected"> + [selected]="_selected" + (selectedChange)="_selectAndClose($event)"> diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 03f91edb4ecf..9dee866200ef 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -117,13 +117,13 @@ describe('MdDatepicker', () => { expect(document.querySelector('md-dialog-container')).not.toBeNull(); expect(testComponent.datepickerInput.value).toEqual(new SimpleDate(2020, 0, 1)); - let selected = new SimpleDate(2017, 0, 1); - testComponent.datepicker._selected = selected; + let cells = document.querySelectorAll('.mat-calendar-table-cell'); + dispatchMouseEvent(cells[1], 'click'); fixture.detectChanges(); fixture.whenStable().then(() => { expect(document.querySelector('md-dialog-container')).toBeNull(); - expect(testComponent.datepickerInput.value).toEqual(selected); + expect(testComponent.datepickerInput.value).toEqual(new SimpleDate(2020, 0, 2)); }); })); @@ -228,7 +228,7 @@ describe('MdDatepicker', () => { expect(testComponent.datepickerInput.value).toBeNull(); let selected = new SimpleDate(2017, 0, 1); - testComponent.datepicker._selected = selected; + testComponent.datepicker._selectAndClose(selected); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -255,7 +255,7 @@ describe('MdDatepicker', () => { expect(inputEl.classList).toContain('ng-pristine'); - testComponent.datepicker._selected = new SimpleDate(2017, 0, 1); + testComponent.datepicker._selectAndClose(new SimpleDate(2017, 0, 1)); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -330,7 +330,7 @@ describe('MdDatepicker', () => { expect(testComponent.datepickerInput.value).toBeNull(); let selected = new SimpleDate(2017, 0, 1); - testComponent.datepicker._selected = selected; + testComponent.datepicker._selectAndClose(selected); fixture.detectChanges(); expect(testComponent.formControl.value).toEqual(selected); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index ec39254664ec..79d8f8401480 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -28,6 +28,7 @@ import {SimpleDate} from '../core/datetime/simple-date'; import {MdDatepickerInput} from './datepicker-input'; import {CalendarLocale} from '../core/datetime/calendar-locale'; import 'rxjs/add/operator/first'; +import {Subscription} from 'rxjs/Subscription'; /** Used to generate a unique ID for each datepicker instance. */ @@ -81,13 +82,7 @@ export class MdDatepicker implements OnDestroy { id = `md-datepicker-${datepickerUid++}`; /** The currently selected date. */ - get _selected(): SimpleDate { - return this._datepickerInput ? this._datepickerInput.value : null; - } - set _selected(value: SimpleDate) { - this.selectedChanged.emit(value); - this.close(); - } + _selected: SimpleDate = null; /** The minimum selectable date. */ get _minDate(): SimpleDate { @@ -114,6 +109,8 @@ export class MdDatepicker implements OnDestroy { /** The input element this datepicker is associated with. */ private _datepickerInput: MdDatepickerInput; + private _inputSubscription: Subscription; + constructor(private _dialog: MdDialog, private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, private _locale: CalendarLocale, @Optional() private _dir: Dir) {} @@ -123,6 +120,19 @@ export class MdDatepicker implements OnDestroy { if (this._popupRef) { this._popupRef.dispose(); } + if (this._inputSubscription) { + this._inputSubscription.unsubscribe(); + } + } + + /** Selects the given date and closes the currently open popup or dialog. */ + _selectAndClose(date: SimpleDate): void { + let oldValue = this._selected; + this._selected = date; + if (!SimpleDate.equals(oldValue, this._selected)) { + this.selectedChanged.emit(date); + } + this.close(); } /** @@ -134,6 +144,8 @@ export class MdDatepicker implements OnDestroy { throw new MdError('An MdDatepicker can only be associated with a single input.'); } this._datepickerInput = input; + this._inputSubscription = + this._datepickerInput._valueChange.subscribe((value: SimpleDate) => this._selected = value); } /**