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. */