diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index 1e605208ec70..aa63ee703de1 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -40,6 +40,9 @@

Result

placeholder="Pick a date" (dateInput)="onDateInput($event)" (dateChange)="onDateChange($event)"> + + "{{resultPickerModel.getError('mdDatepickerParse').text}}" is not a valid date! + Too early! Too late! Date unavailable! diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts index b304da615075..407ed09daeec 100644 --- a/src/lib/core/datetime/date-adapter.ts +++ b/src/lib/core/datetime/date-adapter.ts @@ -107,15 +107,15 @@ export abstract class DateAdapter { * @param value The value to parse. * @param parseFormat The expected format of the value being parsed * (type is implementation-dependent). - * @returns The parsed date, or null if date could not be parsed. + * @returns The parsed date. */ abstract parse(value: any, parseFormat: any): D | null; /** * Formats a date as a string. - * @param date The value to parse. + * @param date The value to format. * @param displayFormat The format to use to display the date as a string. - * @returns The parsed date, or null if date could not be parsed. + * @returns The formatted date string. */ abstract format(date: D, displayFormat: any): string; @@ -156,6 +156,20 @@ export abstract class DateAdapter { */ abstract getISODateString(date: D): string; + /** + * Checks whether the given object is considered a date instance by this DateAdapter. + * @param obj The object to check + * @returns Whether the object is a date instance. + */ + abstract isDateInstance(obj: any): boolean; + + /** + * Checks whether the given date is valid. + * @param date The date to check. + * @returns Whether the date is valid. + */ + abstract isValid(date: D): boolean; + /** * Sets the locale used for all dates. * @param locale The new locale. diff --git a/src/lib/core/datetime/native-date-adapter.spec.ts b/src/lib/core/datetime/native-date-adapter.spec.ts index 7100771f7aba..7f9931310614 100644 --- a/src/lib/core/datetime/native-date-adapter.spec.ts +++ b/src/lib/core/datetime/native-date-adapter.spec.ts @@ -196,8 +196,13 @@ describe('NativeDateAdapter', () => { expect(adapter.parse(date)).not.toBe(date); }); - it('should parse invalid value as null', () => { - expect(adapter.parse('hello')).toBeNull(); + it('should parse invalid value as invalid', () => { + let d = adapter.parse('hello'); + expect(d).not.toBeNull(); + expect(adapter.isDateInstance(d)) + .toBe(true, 'Expected string to have been fed through Date.parse'); + expect(adapter.isValid(d as Date)) + .toBe(false, 'Expected to parse as "invalid date" object'); }); it('should format', () => { @@ -238,6 +243,11 @@ describe('NativeDateAdapter', () => { } }); + it('should throw when attempting to format invalid date', () => { + expect(() => adapter.format(new Date(NaN), {})) + .toThrowError(/NativeDateAdapter: Cannot format invalid date\./); + }); + it('should add years', () => { expect(adapter.addCalendarYears(new Date(2017, JAN, 1), 1)).toEqual(new Date(2018, JAN, 1)); expect(adapter.addCalendarYears(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, JAN, 1)); @@ -304,6 +314,23 @@ describe('NativeDateAdapter', () => { expect(adapter.format(new Date(1800, 7, 14), {day: 'numeric'})).toBe('Thu Aug 14 1800'); } }); + + it('should count today as a valid date instance', () => { + let d = new Date(); + expect(adapter.isValid(d)).toBe(true); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count an invalid date as an invalid date instance', () => { + let d = new Date(NaN); + expect(adapter.isValid(d)).toBe(false); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count a string as not a date instance', () => { + let d = '1/1/2017'; + expect(adapter.isDateInstance(d)).toBe(false); + }); }); diff --git a/src/lib/core/datetime/native-date-adapter.ts b/src/lib/core/datetime/native-date-adapter.ts index 2269942d050e..ce1a2c77653d 100644 --- a/src/lib/core/datetime/native-date-adapter.ts +++ b/src/lib/core/datetime/native-date-adapter.ts @@ -157,11 +157,16 @@ export class NativeDateAdapter extends DateAdapter { parse(value: any): Date | null { // We have no way using the native JS Date to set the parse format or locale, so we ignore these // parameters. - let timestamp = typeof value == 'number' ? value : Date.parse(value); - return isNaN(timestamp) ? null : new Date(timestamp); + if (typeof value == 'number') { + return new Date(value); + } + return value ? new Date(Date.parse(value)) : null; } format(date: Date, displayFormat: Object): string { + if (!this.isValid(date)) { + throw Error('NativeDateAdapter: Cannot format invalid date.'); + } if (SUPPORTS_INTL_API) { if (this.useUtcForDisplay) { date = new Date(Date.UTC( @@ -207,6 +212,14 @@ export class NativeDateAdapter extends DateAdapter { ].join('-'); } + isDateInstance(obj: any) { + return obj instanceof Date; + } + + isValid(date: Date) { + return !isNaN(date.getTime()); + } + /** Creates a date but allows the month and date to overflow. */ private _createDateWithOverflow(year: number, month: number, date: number) { let result = new Date(year, month, date); diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 2b21bbc884f2..897d96241a97 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -66,13 +66,13 @@ export class MdCalendar implements AfterContentInit, OnDestroy { @Input() startView: 'month' | 'year' = 'month'; /** The currently selected date. */ - @Input() selected: D; + @Input() selected: D | null; /** The minimum selectable date. */ - @Input() minDate: D; + @Input() minDate: D | null; /** The maximum selectable date. */ - @Input() maxDate: D; + @Input() maxDate: D | null; /** A function used to filter which dates are selectable. */ @Input() dateFilter: (date: D) => boolean; diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index 1302cff20a0a..156e481f9746 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -112,36 +112,41 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces /** The value of the input. */ @Input() get value(): D | null { - return this._dateAdapter.parse(this._elementRef.nativeElement.value, - this._dateFormats.parse.dateInput); + return this._getValidDateOrNull(this._dateAdapter.parse( + this._elementRef.nativeElement.value, this._dateFormats.parse.dateInput)); } set value(value: D | null) { - let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput); + if (value != null && !this._dateAdapter.isDateInstance(value)) { + throw Error('Datepicker: value not recognized as a date object by DateAdapter.'); + } + this._lastValueValid = !value || this._dateAdapter.isValid(value); + value = this._getValidDateOrNull(value); + let oldDate = this.value; this._renderer.setProperty(this._elementRef.nativeElement, 'value', - date ? this._dateAdapter.format(date, this._dateFormats.display.dateInput) : ''); - if (!this._dateAdapter.sameDate(oldDate, date)) { - this._valueChange.emit(date); + value ? this._dateAdapter.format(value, this._dateFormats.display.dateInput) : ''); + if (!this._dateAdapter.sameDate(oldDate, value)) { + this._valueChange.emit(value); } } /** The minimum valid date. */ @Input() - get min(): D { return this._min; } - set min(value: D) { + get min(): D | null { return this._min; } + set min(value: D | null) { this._min = value; this._validatorOnChange(); } - private _min: D; + private _min: D | null; /** The maximum valid date. */ @Input() - get max(): D { return this._max; } - set max(value: D) { + get max(): D | null { return this._max; } + set max(value: D | null) { this._max = value; this._validatorOnChange(); } - private _max: D; + private _max: D | null; /** Whether the datepicker-input is disabled. */ @Input() @@ -168,6 +173,12 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces private _datepickerSubscription: Subscription; + /** The form control validator for whether the input parses. */ + private _parseValidator: ValidatorFn = (): ValidationErrors | null => { + return this._lastValueValid ? + null : {'mdDatepickerParse': {'text': this._elementRef.nativeElement.value}}; + } + /** The form control validator for the min date. */ private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { return (!this.min || !control.value || @@ -190,7 +201,11 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces /** The combined form control validator for this input. */ private _validator: ValidatorFn | null = - Validators.compose([this._minValidator, this._maxValidator, this._filterValidator]); + Validators.compose( + [this._parseValidator, this._minValidator, this._maxValidator, this._filterValidator]); + + /** Whether the last value set on the input was valid. */ + private _lastValueValid = false; constructor( private _elementRef: ElementRef, @@ -270,6 +285,8 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces _onInput(value: string) { let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput); + this._lastValueValid = !date || this._dateAdapter.isValid(date); + date = this._getValidDateOrNull(date); this._cvaOnChange(date); this._valueChange.emit(date); this.dateInput.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement)); @@ -278,4 +295,12 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces _onChange() { this.dateChange.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement)); } + + /** + * @param obj The object to check. + * @returns The given object if it is both a date instance and valid, otherwise null. + */ + private _getValidDateOrNull(obj: any): D | null { + return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; + } } diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 224cfd72a40d..6f8a891fca51 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -223,6 +223,15 @@ describe('MdDatepicker', () => { expect(ownedElement).not.toBeNull(); expect((ownedElement as Element).tagName.toLowerCase()).toBe('md-calendar'); }); + + it('should throw when given wrong data type', () => { + testComponent.date = '1/1/2017' as any; + + expect(() => fixture.detectChanges()) + .toThrowError(/Datepicker: value not recognized as a date object by DateAdapter\./); + + testComponent.date = null; + }); }); describe('datepicker with too many inputs', () => { @@ -902,7 +911,7 @@ describe('MdDatepicker', () => { class StandardDatepicker { touch = false; disabled = false; - date = new Date(2020, JAN, 1); + date: Date | null = new Date(2020, JAN, 1); @ViewChild('d') datepicker: MdDatepicker; @ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput; } @@ -1008,7 +1017,7 @@ class InputContainerDatepicker { }) class DatepickerWithMinAndMaxValidation { @ViewChild('d') datepicker: MdDatepicker; - date: Date; + date: Date | null; minDate = new Date(2010, JAN, 1); maxDate = new Date(2020, JAN, 1); } diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index fbd815cb2871..2200b1c0eaea 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -124,13 +124,13 @@ export class MdDatepickerContent implements AfterContentInit { export class MdDatepicker implements OnDestroy { /** The date to open the calendar to initially. */ @Input() - get startAt(): D { + get startAt(): D | null { // If an explicit startAt is set we start there, otherwise we start at whatever the currently // selected value is. return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null); } - set startAt(date: D) { this._startAt = date; } - private _startAt: D; + set startAt(date: D | null) { this._startAt = date; } + private _startAt: D | null; /** The view that the calendar should start in. */ @Input() startView: 'month' | 'year' = 'month'; @@ -164,15 +164,17 @@ export class MdDatepicker implements OnDestroy { id = `md-datepicker-${datepickerUid++}`; /** The currently selected date. */ - _selected: D | null = null; + get _selected(): D | null { return this._validSelected; } + set _selected(value: D | null) { this._validSelected = value; } + private _validSelected: D | null = null; /** The minimum selectable date. */ - get _minDate(): D { + get _minDate(): D | null { return this._datepickerInput && this._datepickerInput.min; } /** The maximum selectable date. */ - get _maxDate(): D { + get _maxDate(): D | null { return this._datepickerInput && this._datepickerInput.max; } @@ -240,7 +242,7 @@ export class MdDatepicker implements OnDestroy { } this._datepickerInput = input; this._inputSubscription = - this._datepickerInput._valueChange.subscribe((value: D) => this._selected = value); + this._datepickerInput._valueChange.subscribe((value: D | null) => this._selected = value); } /** Open the calendar. */