From 74a764c8cd945647aa9dc078ea3eb6ada5f8fdcd Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 26 Sep 2017 16:08:13 -0700 Subject: [PATCH 1/4] fix(datepicker): allow `DateAdapter` authors to have more control over what can/can't be coerced to a date BREAKING CHANGES: - `fromIso8601` method on `DateAdapter` removed in favor of `coerceToDate` --- src/lib/core/datetime/date-adapter.ts | 24 ++++++--- .../core/datetime/native-date-adapter.spec.ts | 22 ++++---- src/lib/core/datetime/native-date-adapter.ts | 29 ++++++---- src/lib/datepicker/calendar.ts | 9 ++-- .../datepicker/coerce-date-property.spec.ts | 54 ------------------- src/lib/datepicker/coerce-date-property.ts | 35 ------------ src/lib/datepicker/datepicker-input.ts | 13 +++-- src/lib/datepicker/datepicker.spec.ts | 5 +- src/lib/datepicker/datepicker.ts | 3 +- src/lib/datepicker/month-view.ts | 5 +- src/lib/datepicker/public-api.ts | 1 - src/lib/datepicker/year-view.ts | 5 +- .../adapter/moment-date-adapter.spec.ts | 30 ++++++----- .../adapter/moment-date-adapter.ts | 27 ++++++++-- 14 files changed, 105 insertions(+), 157 deletions(-) diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts index 4c3dc8572a52..58712b985230 100644 --- a/src/lib/core/datetime/date-adapter.ts +++ b/src/lib/core/datetime/date-adapter.ts @@ -170,13 +170,6 @@ export abstract class DateAdapter { */ abstract toIso8601(date: D): string; - /** - * Creates a date from an RFC 3339 compatible string (https://tools.ietf.org/html/rfc3339). - * @param iso8601String The ISO date string to create a date from - * @returns The date created from the ISO date string. - */ - abstract fromIso8601(iso8601String: string): D | null; - /** * Checks whether the given object is considered a date instance by this DateAdapter. * @param obj The object to check @@ -191,6 +184,23 @@ export abstract class DateAdapter { */ abstract isValid(date: D): boolean; + /** + * Attempts to coerce a value to a valid date object. This is different from parsing in that it + * should only coerce non-ambiguous, locale-independent values (e.g. a ISO 8601 string). + * The default implementation does not allow any coercion, it simply checks that the given value + * is already a valid date object or null. + * @param value The value to be coerced to a date object. + * @returns The coerced date object, either a valid date, null if the value can be coerced to a + * null date (e.g. the empty string). + * @throws If the given value cannot be coerced to a valid date or null. + */ + coerceToDate(value: any): D | null { + if (value == null || this.isDateInstance(value) && this.isValid(value)) { + return value; + } + throw Error(`Could not coerce "${value}" to a valid date object.`); + } + /** * 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 92371ee0742f..c2307ace59b5 100644 --- a/src/lib/core/datetime/native-date-adapter.spec.ts +++ b/src/lib/core/datetime/native-date-adapter.spec.ts @@ -6,6 +6,7 @@ import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from const SUPPORTS_INTL = typeof Intl != 'undefined'; + describe('NativeDateAdapter', () => { const platform = new Platform(); let adapter: NativeDateAdapter; @@ -333,14 +334,18 @@ describe('NativeDateAdapter', () => { }); it('should create dates from valid ISO strings', () => { - expect(adapter.fromIso8601('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.fromIso8601('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.fromIso8601('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(adapter.fromIso8601('2017-01-01')).not.toBeNull(); - expect(adapter.fromIso8601('2017-01-01T00:00:00')).not.toBeNull(); - expect(adapter.fromIso8601('1990-13-31T23:59:00Z')).toBeNull(); - expect(adapter.fromIso8601('1/1/2017')).toBeNull(); - expect(adapter.fromIso8601('2017-01-01T')).toBeNull(); + expect(adapter.coerceToDate('1985-04-12T23:20:50.52Z')).not.toBeNull(); + expect(adapter.coerceToDate('1996-12-19T16:39:57-08:00')).not.toBeNull(); + expect(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); + expect(adapter.coerceToDate('2017-01-01')).not.toBeNull(); + expect(adapter.coerceToDate('2017-01-01T00:00:00')).not.toBeNull(); + expect(() => adapter.coerceToDate('1990-13-31T23:59:00Z')).toThrow(); + expect(() => adapter.coerceToDate('1/1/2017')).toThrow(); + expect(() => adapter.coerceToDate('2017-01-01T')).toThrow(); + expect(adapter.coerceToDate('')).toBeNull(); + expect(adapter.coerceToDate(null)).toBeNull(); + expect(adapter.coerceToDate(new Date())).not.toBeNull(); + expect(() => adapter.coerceToDate(new Date(NaN))).toThrow(); }); }); @@ -390,5 +395,4 @@ describe('NativeDateAdapter with LOCALE_ID override', () => { expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue); }); - }); diff --git a/src/lib/core/datetime/native-date-adapter.ts b/src/lib/core/datetime/native-date-adapter.ts index 216f17d35cfb..62eaf71ca03b 100644 --- a/src/lib/core/datetime/native-date-adapter.ts +++ b/src/lib/core/datetime/native-date-adapter.ts @@ -7,8 +7,9 @@ */ import {Inject, Injectable, Optional} from '@angular/core'; -import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter'; import {extendObject} from '../util/object-extend'; +import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter'; + // TODO(mmalerba): Remove when we no longer support safari 9. /** Whether the browser supports the Intl API. */ @@ -219,16 +220,26 @@ export class NativeDateAdapter extends DateAdapter { ].join('-'); } - fromIso8601(iso8601String: string): Date | null { - // The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the - // string is the right format first. - if (ISO_8601_REGEX.test(iso8601String)) { - let d = new Date(iso8601String); - if (this.isValid(d)) { - return d; + /** + * Returns the given value if given a valid Date or null. Coerces valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) to valid Dates and empty string to null. Throws on all + * other values. + */ + coerceToDate(value: any): Date | null { + if (typeof value === 'string') { + if (!value) { + return null; + } + // The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the + // string is the right format first. + if (ISO_8601_REGEX.test(value)) { + let date = new Date(value); + if (this.isValid(date)) { + return date; + } } } - return null; + return super.coerceToDate(value); } isDateInstance(obj: any) { diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index fec74a95466e..704d502f889f 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -38,7 +38,6 @@ import { import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {first} from 'rxjs/operator/first'; import {Subscription} from 'rxjs/Subscription'; -import {coerceDateProperty} from './coerce-date-property'; import {createMissingDateImplError} from './datepicker-errors'; import {MatDatepickerIntl} from './datepicker-intl'; import {MatMonthView} from './month-view'; @@ -67,7 +66,7 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** A date representing the period (month or year) to start the calendar in. */ @Input() get startAt(): D | null { return this._startAt; } - set startAt(value: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, value); } + set startAt(value: D | null) { this._startAt = this._dateAdapter.coerceToDate(value); } private _startAt: D | null; /** Whether the calendar should be started in month or year view. */ @@ -76,19 +75,19 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** The currently selected date. */ @Input() get selected(): D | null { return this._selected; } - set selected(value: D | null) { this._selected = coerceDateProperty(this._dateAdapter, value); } + set selected(value: D | null) { this._selected = this._dateAdapter.coerceToDate(value); } private _selected: D | null; /** The minimum selectable date. */ @Input() get minDate(): D | null { return this._minDate; } - set minDate(value: D | null) { this._minDate = coerceDateProperty(this._dateAdapter, value); } + set minDate(value: D | null) { this._minDate = this._dateAdapter.coerceToDate(value); } private _minDate: D | null; /** The maximum selectable date. */ @Input() get maxDate(): D | null { return this._maxDate; } - set maxDate(value: D | null) { this._maxDate = coerceDateProperty(this._dateAdapter, value); } + set maxDate(value: D | null) { this._maxDate = this._dateAdapter.coerceToDate(value); } private _maxDate: D | null; /** A function used to filter which dates are selectable. */ diff --git a/src/lib/datepicker/coerce-date-property.spec.ts b/src/lib/datepicker/coerce-date-property.spec.ts index cad4486f843c..e69de29bb2d1 100644 --- a/src/lib/datepicker/coerce-date-property.spec.ts +++ b/src/lib/datepicker/coerce-date-property.spec.ts @@ -1,54 +0,0 @@ -import {async, inject, TestBed} from '@angular/core/testing'; -import {DateAdapter, JAN, MatNativeDateModule} from '@angular/material/core'; -import {coerceDateProperty} from './index'; - - -describe('coerceDateProperty', () => { - let adapter: DateAdapter; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [MatNativeDateModule], - }); - - TestBed.compileComponents(); - })); - - beforeEach(inject([DateAdapter], (dateAdapter: DateAdapter) => { - adapter = dateAdapter; - })); - - it('should pass through existing date', () => { - const d = new Date(2017, JAN, 1); - expect(coerceDateProperty(adapter, d)).toBe(d); - }); - - it('should pass through invalid date', () => { - const d = new Date(NaN); - expect(coerceDateProperty(adapter, d)).toBe(d); - }); - - it('should pass through null and undefined', () => { - expect(coerceDateProperty(adapter, null)).toBeNull(); - expect(coerceDateProperty(adapter, undefined)).toBeUndefined(); - }); - - it('should coerce empty string to null', () => { - expect(coerceDateProperty(adapter, '')).toBe(null); - }); - - it('should coerce ISO 8601 string to date', () => { - let isoString = '2017-01-01T00:00:00Z'; - expect(coerceDateProperty(adapter, isoString)).toEqual(new Date(isoString)); - }); - - it('should throw when given a number', () => { - expect(() => coerceDateProperty(adapter, 5)).toThrow(); - expect(() => coerceDateProperty(adapter, 0)).toThrow(); - }); - - it('should throw when given a string with incorrect format', () => { - expect(() => coerceDateProperty(adapter, '1/1/2017')).toThrow(); - expect(() => coerceDateProperty(adapter, 'hello')).toThrow(); - }); -}); diff --git a/src/lib/datepicker/coerce-date-property.ts b/src/lib/datepicker/coerce-date-property.ts index d85a54f06021..e69de29bb2d1 100644 --- a/src/lib/datepicker/coerce-date-property.ts +++ b/src/lib/datepicker/coerce-date-property.ts @@ -1,35 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {DateAdapter} from '@angular/material/core'; - - -/** - * Function that attempts to coerce a value to a date using a DateAdapter. Date instances, null, - * and undefined will be passed through. Empty strings will be coerced to null. Valid ISO 8601 - * strings (https://www.ietf.org/rfc/rfc3339.txt) will be coerced to dates. All other values will - * result in an error being thrown. - * @param adapter The date adapter to use for coercion - * @param value The value to coerce. - * @return A date object coerced from the value. - * @throws Throws when the value cannot be coerced. - */ -export function coerceDateProperty(adapter: DateAdapter, value: any): D | null { - if (typeof value === 'string') { - if (value == '') { - value = null; - } else { - value = adapter.fromIso8601(value) || value; - } - } - if (value == null || adapter.isDateInstance(value)) { - return value; - } - throw Error(`Datepicker: Value must be either a date object recognized by the DateAdapter or ` + - `an ISO 8601 string. Instead got: ${value}`); -} diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index d1ee19ad40bd..67131de68734 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -34,7 +34,6 @@ import { import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {MatFormField} from '@angular/material/form-field'; import {Subscription} from 'rxjs/Subscription'; -import {coerceDateProperty} from './coerce-date-property'; import {MatDatepicker} from './datepicker'; import {createMissingDateImplError} from './datepicker-errors'; @@ -113,7 +112,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce return this._value; } set value(value: D | null) { - value = coerceDateProperty(this._dateAdapter, value); + value = this._dateAdapter.coerceToDate(value); this._lastValueValid = !value || this._dateAdapter.isValid(value); value = this._getValidDateOrNull(value); @@ -131,7 +130,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get min(): D | null { return this._min; } set min(value: D | null) { - this._min = coerceDateProperty(this._dateAdapter, value); + this._min = this._dateAdapter.coerceToDate(value); this._validatorOnChange(); } private _min: D | null; @@ -140,7 +139,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get max(): D | null { return this._max; } set max(value: D | null) { - this._max = coerceDateProperty(this._dateAdapter, value); + this._max = this._dateAdapter.coerceToDate(value); this._validatorOnChange(); } private _max: D | null; @@ -188,7 +187,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the min date. */ private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = coerceDateProperty(this._dateAdapter, control.value); + const controlValue = this._dateAdapter.coerceToDate(control.value); return (!this.min || !controlValue || this._dateAdapter.compareDate(this.min, controlValue) <= 0) ? null : {'matDatepickerMin': {'min': this.min, 'actual': controlValue}}; @@ -196,7 +195,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the max date. */ private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = coerceDateProperty(this._dateAdapter, control.value); + const controlValue = this._dateAdapter.coerceToDate(control.value); return (!this.max || !controlValue || this._dateAdapter.compareDate(this.max, controlValue) >= 0) ? null : {'matDatepickerMax': {'max': this.max, 'actual': controlValue}}; @@ -204,7 +203,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the date filter. */ private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = coerceDateProperty(this._dateAdapter, control.value); + const controlValue = this._dateAdapter.coerceToDate(control.value); return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ? null : {'matDatepickerFilter': true}; } diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 919539a44db3..5eaaa23539d6 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -275,9 +275,8 @@ describe('MatDatepicker', () => { it('should throw when given wrong data type', () => { testComponent.date = '1/1/2017' as any; - expect(() => fixture.detectChanges()).toThrowError( - 'Datepicker: Value must be either a date object recognized by the DateAdapter or an ' + - 'ISO 8601 string. Instead got: 1/1/2017'); + expect(() => fixture.detectChanges()) + .toThrowError('Could not coerce "1/1/2017" to a valid date object.'); testComponent.date = null; }); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 5663e0526202..6a14c0c1c708 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -42,7 +42,6 @@ import {DOCUMENT} from '@angular/platform-browser'; import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; import {MatCalendar} from './calendar'; -import {coerceDateProperty} from './coerce-date-property'; import {createMissingDateImplError} from './datepicker-errors'; import {MatDatepickerInput} from './datepicker-input'; @@ -133,7 +132,7 @@ export class MatDatepicker implements OnDestroy { // selected value is. return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null); } - set startAt(date: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, date); } + set startAt(date: D | null) { this._startAt = this._dateAdapter.coerceToDate(date); } private _startAt: D | null; /** The view that the calendar should start in. */ diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index afbdec8177dc..245219a21489 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -20,7 +20,6 @@ import { } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {MatCalendarCell} from './calendar-body'; -import {coerceDateProperty} from './coerce-date-property'; import {createMissingDateImplError} from './datepicker-errors'; @@ -47,7 +46,7 @@ export class MatMonthView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = coerceDateProperty(this._dateAdapter, value) || this._dateAdapter.today(); + this._activeDate = this._dateAdapter.coerceToDate(value) || this._dateAdapter.today(); if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { this._init(); } @@ -58,7 +57,7 @@ export class MatMonthView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = coerceDateProperty(this._dateAdapter, value); + this._selected = this._dateAdapter.coerceToDate(value); this._selectedDate = this._getDateInCurrentMonth(this._selected); } private _selected: D | null; diff --git a/src/lib/datepicker/public-api.ts b/src/lib/datepicker/public-api.ts index ca079144a7fd..eb79de80b7ce 100644 --- a/src/lib/datepicker/public-api.ts +++ b/src/lib/datepicker/public-api.ts @@ -9,7 +9,6 @@ export * from './datepicker-module'; export * from './calendar'; export * from './calendar-body'; -export * from './coerce-date-property'; export * from './datepicker'; export * from './datepicker-input'; export * from './datepicker-intl'; diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index 2e999a6eff02..945294e0b62e 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -20,7 +20,6 @@ import { } from '@angular/core'; import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core'; import {MatCalendarCell} from './calendar-body'; -import {coerceDateProperty} from './coerce-date-property'; import {createMissingDateImplError} from './datepicker-errors'; @@ -42,7 +41,7 @@ export class MatYearView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = coerceDateProperty(this._dateAdapter, value) || this._dateAdapter.today(); + this._activeDate = this._dateAdapter.coerceToDate(value) || this._dateAdapter.today(); if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) { this._init(); } @@ -53,7 +52,7 @@ export class MatYearView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = coerceDateProperty(this._dateAdapter, value); + this._selected = this._dateAdapter.coerceToDate(value); this._selectedMonth = this._getMonthInCurrentYear(this._selected); } private _selected: D | null; diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts index f9bf51316851..85dc3336a5f5 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts @@ -6,16 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {MomentDateAdapter} from './moment-date-adapter'; -import {async, inject, TestBed} from '@angular/core/testing'; -import {MomentDateModule} from './index'; -import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material'; import {LOCALE_ID} from '@angular/core'; +import {async, inject, TestBed} from '@angular/core/testing'; +import {DateAdapter, DEC, FEB, JAN, MAR, MAT_DATE_LOCALE} from '@angular/material/core'; import * as moment from 'moment'; - - -// Month constants for more readable tests. -const JAN = 0, FEB = 1, MAR = 2, DEC = 11; +import {MomentDateModule} from './index'; +import {MomentDateAdapter} from './moment-date-adapter'; describe('MomentDateAdapter', () => { @@ -309,12 +305,18 @@ describe('MomentDateAdapter', () => { expect(adapter.isDateInstance(d)).toBe(false); }); - it('should create dates from valid ISO strings', () => { - expect(adapter.fromIso8601('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.fromIso8601('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.fromIso8601('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(adapter.fromIso8601('1990-13-31T23:59:00Z')).toBeNull(); - expect(adapter.fromIso8601('1/1/2017')).toBeNull(); + it('should create valid dates from valid ISO strings', () => { + expect(adapter.coerceToDate('1985-04-12T23:20:50.52Z')).not.toBeNull(); + expect(adapter.coerceToDate('1996-12-19T16:39:57-08:00')).not.toBeNull(); + expect(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); + expect(() => adapter.coerceToDate('1990-13-31T23:59:00Z')).toThrow(); + expect(() => adapter.coerceToDate('1/1/2017')).toThrow(); + expect(adapter.coerceToDate('')).toBeNull(); + expect(adapter.coerceToDate(null)).toBeNull(); + expect(adapter.coerceToDate(new Date())).not.toBeNull(); + expect(() => adapter.coerceToDate(new Date(NaN))).toThrow(); + expect(adapter.coerceToDate(moment())).not.toBeNull(); + expect(() => adapter.coerceToDate(moment.invalid())).toThrow(); }); it('setLocale should not modify global moment locale', () => { diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.ts b/src/material-moment-adapter/adapter/moment-date-adapter.ts index fbba8886cce5..4652a29d6a14 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.ts @@ -8,14 +8,14 @@ import {Inject, Injectable, Optional} from '@angular/core'; import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material'; - // Depending on whether rollup is used, moment needs to be imported differently. // Since Moment.js doesn't have a default export, we normally need to import using the `* as` // syntax. However, rollup creates a synthetic default module and we thus need to import it using // the `default as` syntax. // TODO(mmalerba): See if we can clean this up at some point. -import {default as _rollupMoment, Moment} from 'moment'; import * as _moment from 'moment'; +import {default as _rollupMoment, Moment} from 'moment'; + const moment = _rollupMoment || _moment; @@ -174,9 +174,26 @@ export class MomentDateAdapter extends DateAdapter { return this.clone(date).format(); } - fromIso8601(iso8601String: string): Moment | null { - let d = moment(iso8601String, moment.ISO_8601).locale(this.locale); - return this.isValid(d) ? d : null; + /** + * Returns the given value if given a valid Moment or null. Coerces valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects to valid Moments and empty string + * to null. Throws on all other values. + */ + coerceToDate(value: any): Moment | null { + let date; + if (value instanceof Date) { + date = moment(value); + } + if (typeof value === 'string') { + if (!value) { + return null; + } + date = moment(value, moment.ISO_8601).locale(this.locale); + } + if (date && this.isValid(date)) { + return date; + } + return super.coerceToDate(value); } isDateInstance(obj: any): boolean { From 468a0ab8fe95531265985b29526bd2e0d9d80cc0 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 11 Oct 2017 15:47:39 -0700 Subject: [PATCH 2/4] address comments --- src/lib/core/datetime/date-adapter.ts | 29 +++++++++++-------- .../core/datetime/native-date-adapter.spec.ts | 24 +++++++-------- src/lib/core/datetime/native-date-adapter.ts | 10 +++---- src/lib/datepicker/calendar.ts | 8 ++--- .../datepicker/coerce-date-property.spec.ts | 0 src/lib/datepicker/coerce-date-property.ts | 0 src/lib/datepicker/datepicker-input.ts | 12 ++++---- src/lib/datepicker/datepicker.spec.ts | 2 +- src/lib/datepicker/datepicker.ts | 2 +- src/lib/datepicker/month-view.ts | 4 +-- src/lib/datepicker/year-view.ts | 4 +-- .../adapter/moment-date-adapter.spec.ts | 22 +++++++------- .../adapter/moment-date-adapter.ts | 10 +++---- 13 files changed, 66 insertions(+), 61 deletions(-) delete mode 100644 src/lib/datepicker/coerce-date-property.spec.ts delete mode 100644 src/lib/datepicker/coerce-date-property.ts diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts index 58712b985230..c801be5be5c6 100644 --- a/src/lib/core/datetime/date-adapter.ts +++ b/src/lib/core/datetime/date-adapter.ts @@ -118,7 +118,7 @@ export abstract class DateAdapter { abstract today(): D; /** - * Parses a date from a value. + * Parses a date from a user-inputted value. * @param value The value to parse. * @param parseFormat The expected format of the value being parsed * (type is implementation-dependent). @@ -127,7 +127,7 @@ export abstract class DateAdapter { abstract parse(value: any, parseFormat: any): D | null; /** - * Formats a date as a string. + * Formats a date as a string according to the given format. * @param date The value to format. * @param displayFormat The format to use to display the date as a string. * @returns The formatted date string. @@ -165,6 +165,8 @@ export abstract class DateAdapter { /** * Gets the RFC 3339 compatible string (https://tools.ietf.org/html/rfc3339) for the given date. + * This method is used to generate date strings that are compatible with native HTML attributes + * such as the `min` or `max` attribute of an ``. * @param date The date to get the ISO date string for. * @returns The ISO date string date string. */ @@ -185,20 +187,23 @@ export abstract class DateAdapter { abstract isValid(date: D): boolean; /** - * Attempts to coerce a value to a valid date object. This is different from parsing in that it - * should only coerce non-ambiguous, locale-independent values (e.g. a ISO 8601 string). - * The default implementation does not allow any coercion, it simply checks that the given value - * is already a valid date object or null. - * @param value The value to be coerced to a date object. - * @returns The coerced date object, either a valid date, null if the value can be coerced to a - * null date (e.g. the empty string). - * @throws If the given value cannot be coerced to a valid date or null. + * Attempts to deserialize a value to a valid date object. This is different from parsing in that + * deserialize should only accept non-ambiguous, locale-independent values (e.g. a ISO 8601 + * string). The default implementation does not allow any deserialization, it simply checks that + * the given value is already a valid date object or null. The `` will call this + * method on all of it's `@Input()` properties that accept dates. It is therefore possible to + * support passing your wire format directly to these properties by overriding this method to + * also deserialize your wire format. + * @param value The value to be deserialized into a date object. + * @returns The deserialized date object, either a valid date, null if the value can be + * deserialized into a null date (e.g. the empty string). + * @throws If the given value cannot be deserialized into a valid date or null. */ - coerceToDate(value: any): D | null { + deserialize(value: any): D | null { if (value == null || this.isDateInstance(value) && this.isValid(value)) { return value; } - throw Error(`Could not coerce "${value}" to a valid date object.`); + throw Error(`Could not deserialize "${value}" into a valid date object.`); } /** diff --git a/src/lib/core/datetime/native-date-adapter.spec.ts b/src/lib/core/datetime/native-date-adapter.spec.ts index c2307ace59b5..d331393e4bab 100644 --- a/src/lib/core/datetime/native-date-adapter.spec.ts +++ b/src/lib/core/datetime/native-date-adapter.spec.ts @@ -334,18 +334,18 @@ describe('NativeDateAdapter', () => { }); it('should create dates from valid ISO strings', () => { - expect(adapter.coerceToDate('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.coerceToDate('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(adapter.coerceToDate('2017-01-01')).not.toBeNull(); - expect(adapter.coerceToDate('2017-01-01T00:00:00')).not.toBeNull(); - expect(() => adapter.coerceToDate('1990-13-31T23:59:00Z')).toThrow(); - expect(() => adapter.coerceToDate('1/1/2017')).toThrow(); - expect(() => adapter.coerceToDate('2017-01-01T')).toThrow(); - expect(adapter.coerceToDate('')).toBeNull(); - expect(adapter.coerceToDate(null)).toBeNull(); - expect(adapter.coerceToDate(new Date())).not.toBeNull(); - expect(() => adapter.coerceToDate(new Date(NaN))).toThrow(); + expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull(); + expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull(); + expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); + expect(adapter.deserialize('2017-01-01')).not.toBeNull(); + expect(adapter.deserialize('2017-01-01T00:00:00')).not.toBeNull(); + expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow(); + expect(() => adapter.deserialize('1/1/2017')).toThrow(); + expect(() => adapter.deserialize('2017-01-01T')).toThrow(); + expect(adapter.deserialize('')).toBeNull(); + expect(adapter.deserialize(null)).toBeNull(); + expect(adapter.deserialize(new Date())).not.toBeNull(); + expect(() => adapter.deserialize(new Date(NaN))).toThrow(); }); }); diff --git a/src/lib/core/datetime/native-date-adapter.ts b/src/lib/core/datetime/native-date-adapter.ts index 62eaf71ca03b..81458c70f28b 100644 --- a/src/lib/core/datetime/native-date-adapter.ts +++ b/src/lib/core/datetime/native-date-adapter.ts @@ -221,11 +221,11 @@ export class NativeDateAdapter extends DateAdapter { } /** - * Returns the given value if given a valid Date or null. Coerces valid ISO 8601 strings - * (https://www.ietf.org/rfc/rfc3339.txt) to valid Dates and empty string to null. Throws on all - * other values. + * Returns the given value if given a valid Date or null. Deserializes valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Throws on + * all other values. */ - coerceToDate(value: any): Date | null { + deserialize(value: any): Date | null { if (typeof value === 'string') { if (!value) { return null; @@ -239,7 +239,7 @@ export class NativeDateAdapter extends DateAdapter { } } } - return super.coerceToDate(value); + return super.deserialize(value); } isDateInstance(obj: any) { diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index 704d502f889f..1c47c8399c8b 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -66,7 +66,7 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** A date representing the period (month or year) to start the calendar in. */ @Input() get startAt(): D | null { return this._startAt; } - set startAt(value: D | null) { this._startAt = this._dateAdapter.coerceToDate(value); } + set startAt(value: D | null) { this._startAt = this._dateAdapter.deserialize(value); } private _startAt: D | null; /** Whether the calendar should be started in month or year view. */ @@ -75,19 +75,19 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** The currently selected date. */ @Input() get selected(): D | null { return this._selected; } - set selected(value: D | null) { this._selected = this._dateAdapter.coerceToDate(value); } + set selected(value: D | null) { this._selected = this._dateAdapter.deserialize(value); } private _selected: D | null; /** The minimum selectable date. */ @Input() get minDate(): D | null { return this._minDate; } - set minDate(value: D | null) { this._minDate = this._dateAdapter.coerceToDate(value); } + set minDate(value: D | null) { this._minDate = this._dateAdapter.deserialize(value); } private _minDate: D | null; /** The maximum selectable date. */ @Input() get maxDate(): D | null { return this._maxDate; } - set maxDate(value: D | null) { this._maxDate = this._dateAdapter.coerceToDate(value); } + set maxDate(value: D | null) { this._maxDate = this._dateAdapter.deserialize(value); } private _maxDate: D | null; /** A function used to filter which dates are selectable. */ diff --git a/src/lib/datepicker/coerce-date-property.spec.ts b/src/lib/datepicker/coerce-date-property.spec.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/lib/datepicker/coerce-date-property.ts b/src/lib/datepicker/coerce-date-property.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index 67131de68734..7a30767003c4 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -112,7 +112,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce return this._value; } set value(value: D | null) { - value = this._dateAdapter.coerceToDate(value); + value = this._dateAdapter.deserialize(value); this._lastValueValid = !value || this._dateAdapter.isValid(value); value = this._getValidDateOrNull(value); @@ -130,7 +130,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get min(): D | null { return this._min; } set min(value: D | null) { - this._min = this._dateAdapter.coerceToDate(value); + this._min = this._dateAdapter.deserialize(value); this._validatorOnChange(); } private _min: D | null; @@ -139,7 +139,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get max(): D | null { return this._max; } set max(value: D | null) { - this._max = this._dateAdapter.coerceToDate(value); + this._max = this._dateAdapter.deserialize(value); this._validatorOnChange(); } private _max: D | null; @@ -187,7 +187,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the min date. */ private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.coerceToDate(control.value); + const controlValue = this._dateAdapter.deserialize(control.value); return (!this.min || !controlValue || this._dateAdapter.compareDate(this.min, controlValue) <= 0) ? null : {'matDatepickerMin': {'min': this.min, 'actual': controlValue}}; @@ -195,7 +195,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the max date. */ private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.coerceToDate(control.value); + const controlValue = this._dateAdapter.deserialize(control.value); return (!this.max || !controlValue || this._dateAdapter.compareDate(this.max, controlValue) >= 0) ? null : {'matDatepickerMax': {'max': this.max, 'actual': controlValue}}; @@ -203,7 +203,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the date filter. */ private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.coerceToDate(control.value); + const controlValue = this._dateAdapter.deserialize(control.value); return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ? null : {'matDatepickerFilter': true}; } diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 5eaaa23539d6..ccd2fbfab3a5 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -276,7 +276,7 @@ describe('MatDatepicker', () => { testComponent.date = '1/1/2017' as any; expect(() => fixture.detectChanges()) - .toThrowError('Could not coerce "1/1/2017" to a valid date object.'); + .toThrowError('Could not deserialize "1/1/2017" into a valid date object.'); testComponent.date = null; }); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 6a14c0c1c708..6a09edeb858c 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -132,7 +132,7 @@ export class MatDatepicker implements OnDestroy { // selected value is. return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null); } - set startAt(date: D | null) { this._startAt = this._dateAdapter.coerceToDate(date); } + set startAt(date: D | null) { this._startAt = this._dateAdapter.deserialize(date); } private _startAt: D | null; /** The view that the calendar should start in. */ diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index 245219a21489..356db8365c0d 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -46,7 +46,7 @@ export class MatMonthView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = this._dateAdapter.coerceToDate(value) || this._dateAdapter.today(); + this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today(); if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { this._init(); } @@ -57,7 +57,7 @@ export class MatMonthView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = this._dateAdapter.coerceToDate(value); + this._selected = this._dateAdapter.deserialize(value); this._selectedDate = this._getDateInCurrentMonth(this._selected); } private _selected: D | null; diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index 945294e0b62e..fd6cf6ac5b1c 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -41,7 +41,7 @@ export class MatYearView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = this._dateAdapter.coerceToDate(value) || this._dateAdapter.today(); + this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today(); if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) { this._init(); } @@ -52,7 +52,7 @@ export class MatYearView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = this._dateAdapter.coerceToDate(value); + this._selected = this._dateAdapter.deserialize(value); this._selectedMonth = this._getMonthInCurrentYear(this._selected); } private _selected: D | null; diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts index 85dc3336a5f5..ea3da86a9185 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts @@ -306,17 +306,17 @@ describe('MomentDateAdapter', () => { }); it('should create valid dates from valid ISO strings', () => { - expect(adapter.coerceToDate('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.coerceToDate('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(() => adapter.coerceToDate('1990-13-31T23:59:00Z')).toThrow(); - expect(() => adapter.coerceToDate('1/1/2017')).toThrow(); - expect(adapter.coerceToDate('')).toBeNull(); - expect(adapter.coerceToDate(null)).toBeNull(); - expect(adapter.coerceToDate(new Date())).not.toBeNull(); - expect(() => adapter.coerceToDate(new Date(NaN))).toThrow(); - expect(adapter.coerceToDate(moment())).not.toBeNull(); - expect(() => adapter.coerceToDate(moment.invalid())).toThrow(); + expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull(); + expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull(); + expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); + expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow(); + expect(() => adapter.deserialize('1/1/2017')).toThrow(); + expect(adapter.deserialize('')).toBeNull(); + expect(adapter.deserialize(null)).toBeNull(); + expect(adapter.deserialize(new Date())).not.toBeNull(); + expect(() => adapter.deserialize(new Date(NaN))).toThrow(); + expect(adapter.deserialize(moment())).not.toBeNull(); + expect(() => adapter.deserialize(moment.invalid())).toThrow(); }); it('setLocale should not modify global moment locale', () => { diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.ts b/src/material-moment-adapter/adapter/moment-date-adapter.ts index 4652a29d6a14..1d52c9006725 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.ts @@ -175,11 +175,11 @@ export class MomentDateAdapter extends DateAdapter { } /** - * Returns the given value if given a valid Moment or null. Coerces valid ISO 8601 strings - * (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects to valid Moments and empty string - * to null. Throws on all other values. + * Returns the given value if given a valid Moment or null. Deserializes valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects into valid Moments and empty + * string into null. Throws on all other values. */ - coerceToDate(value: any): Moment | null { + deserialize(value: any): Moment | null { let date; if (value instanceof Date) { date = moment(value); @@ -193,7 +193,7 @@ export class MomentDateAdapter extends DateAdapter { if (date && this.isValid(date)) { return date; } - return super.coerceToDate(value); + return super.deserialize(value); } isDateInstance(obj: any): boolean { From a31aec46509e6568322457b47205963c8324e284 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 12 Oct 2017 11:54:16 -0700 Subject: [PATCH 3/4] more comments addressed --- src/lib/core/datetime/date-adapter.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts index c801be5be5c6..08d860e17434 100644 --- a/src/lib/core/datetime/date-adapter.ts +++ b/src/lib/core/datetime/date-adapter.ts @@ -118,7 +118,7 @@ export abstract class DateAdapter { abstract today(): D; /** - * Parses a date from a user-inputted value. + * Parses a date from a user-provided value. * @param value The value to parse. * @param parseFormat The expected format of the value being parsed * (type is implementation-dependent). @@ -188,12 +188,12 @@ export abstract class DateAdapter { /** * Attempts to deserialize a value to a valid date object. This is different from parsing in that - * deserialize should only accept non-ambiguous, locale-independent values (e.g. a ISO 8601 + * deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601 * string). The default implementation does not allow any deserialization, it simply checks that * the given value is already a valid date object or null. The `` will call this * method on all of it's `@Input()` properties that accept dates. It is therefore possible to - * support passing your wire format directly to these properties by overriding this method to - * also deserialize your wire format. + * support passing values from your backend directly to these properties by overriding this method + * to also deserialize the format used by your backend. * @param value The value to be deserialized into a date object. * @returns The deserialized date object, either a valid date, null if the value can be * deserialized into a null date (e.g. the empty string). From a4585d6ed476a47994d0db8522db4ffee04780be Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 27 Oct 2017 09:59:05 -0700 Subject: [PATCH 4/4] return invalid date instead of throwing --- src/lib/core/datetime/date-adapter.ts | 21 +++++++++--- .../core/datetime/native-date-adapter.spec.ts | 32 +++++++++++++------ src/lib/core/datetime/native-date-adapter.ts | 8 +++-- src/lib/datepicker/calendar.ts | 24 +++++++++++--- src/lib/datepicker/datepicker-input.ts | 11 +++---- src/lib/datepicker/datepicker.spec.ts | 7 ++-- src/lib/datepicker/datepicker.ts | 12 ++++++- src/lib/datepicker/month-view.ts | 13 ++++++-- src/lib/datepicker/year-view.ts | 13 ++++++-- .../adapter/moment-date-adapter.spec.ts | 30 +++++++++++------ .../adapter/moment-date-adapter.ts | 6 +++- 11 files changed, 131 insertions(+), 46 deletions(-) diff --git a/src/lib/core/datetime/date-adapter.ts b/src/lib/core/datetime/date-adapter.ts index 08d860e17434..c6f9da34969e 100644 --- a/src/lib/core/datetime/date-adapter.ts +++ b/src/lib/core/datetime/date-adapter.ts @@ -186,6 +186,12 @@ export abstract class DateAdapter { */ abstract isValid(date: D): boolean; + /** + * Gets date instance that is not valid. + * @returns An invalid date. + */ + abstract invalid(): D; + /** * Attempts to deserialize a value to a valid date object. This is different from parsing in that * deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601 @@ -196,14 +202,13 @@ export abstract class DateAdapter { * to also deserialize the format used by your backend. * @param value The value to be deserialized into a date object. * @returns The deserialized date object, either a valid date, null if the value can be - * deserialized into a null date (e.g. the empty string). - * @throws If the given value cannot be deserialized into a valid date or null. + * deserialized into a null date (e.g. the empty string), or an invalid date. */ deserialize(value: any): D | null { if (value == null || this.isDateInstance(value) && this.isValid(value)) { return value; } - throw Error(`Could not deserialize "${value}" into a valid date object.`); + return this.invalid(); } /** @@ -236,7 +241,15 @@ export abstract class DateAdapter { * Null dates are considered equal to other null dates. */ sameDate(first: D | null, second: D | null): boolean { - return first && second ? !this.compareDate(first, second) : first == second; + if (first && second) { + let firstValid = this.isValid(first); + let secondValid = this.isValid(second); + if (firstValid && secondValid) { + return !this.compareDate(first, second) + } + return firstValid == secondValid; + } + return first == second; } /** diff --git a/src/lib/core/datetime/native-date-adapter.spec.ts b/src/lib/core/datetime/native-date-adapter.spec.ts index d331393e4bab..a745026d2798 100644 --- a/src/lib/core/datetime/native-date-adapter.spec.ts +++ b/src/lib/core/datetime/native-date-adapter.spec.ts @@ -10,6 +10,7 @@ const SUPPORTS_INTL = typeof Intl != 'undefined'; describe('NativeDateAdapter', () => { const platform = new Platform(); let adapter: NativeDateAdapter; + let assertValidDate: (d: Date | null, valid: boolean) => void; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -19,6 +20,13 @@ describe('NativeDateAdapter', () => { beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => { adapter = d; + + assertValidDate = (d: Date | null, valid: boolean) => { + expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`); + expect(adapter.isValid(d!)).toBe(valid, + `Expected ${d} to be ${valid ? 'valid' : 'invalid'},` + + ` but was ${valid ? 'invalid' : 'valid'}`); + } })); it('should get year', () => { @@ -334,18 +342,22 @@ describe('NativeDateAdapter', () => { }); it('should create dates from valid ISO strings', () => { - expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(adapter.deserialize('2017-01-01')).not.toBeNull(); - expect(adapter.deserialize('2017-01-01T00:00:00')).not.toBeNull(); - expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow(); - expect(() => adapter.deserialize('1/1/2017')).toThrow(); - expect(() => adapter.deserialize('2017-01-01T')).toThrow(); + assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true); + assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true); + assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); + assertValidDate(adapter.deserialize('2017-01-01'), true); + assertValidDate(adapter.deserialize('2017-01-01T00:00:00'), true); + assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false); + assertValidDate(adapter.deserialize('1/1/2017'), false); + assertValidDate(adapter.deserialize('2017-01-01T'), false); expect(adapter.deserialize('')).toBeNull(); expect(adapter.deserialize(null)).toBeNull(); - expect(adapter.deserialize(new Date())).not.toBeNull(); - expect(() => adapter.deserialize(new Date(NaN))).toThrow(); + assertValidDate(adapter.deserialize(new Date()), true); + assertValidDate(adapter.deserialize(new Date(NaN)), false); + }); + + it('should create an invalid date', () => { + assertValidDate(adapter.invalid(), false); }); }); diff --git a/src/lib/core/datetime/native-date-adapter.ts b/src/lib/core/datetime/native-date-adapter.ts index 81458c70f28b..22fd954693b2 100644 --- a/src/lib/core/datetime/native-date-adapter.ts +++ b/src/lib/core/datetime/native-date-adapter.ts @@ -222,8 +222,8 @@ export class NativeDateAdapter extends DateAdapter { /** * Returns the given value if given a valid Date or null. Deserializes valid ISO 8601 strings - * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Throws on - * all other values. + * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Returns an + * invalid date for all other values. */ deserialize(value: any): Date | null { if (typeof value === 'string') { @@ -250,6 +250,10 @@ export class NativeDateAdapter extends DateAdapter { return !isNaN(date.getTime()); } + invalid(): Date { + return new Date(NaN); + } + /** 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 1c47c8399c8b..e0f2477f853a 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -66,7 +66,9 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** A date representing the period (month or year) to start the calendar in. */ @Input() get startAt(): D | null { return this._startAt; } - set startAt(value: D | null) { this._startAt = this._dateAdapter.deserialize(value); } + set startAt(value: D | null) { + this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); + } private _startAt: D | null; /** Whether the calendar should be started in month or year view. */ @@ -75,19 +77,25 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { /** The currently selected date. */ @Input() get selected(): D | null { return this._selected; } - set selected(value: D | null) { this._selected = this._dateAdapter.deserialize(value); } + set selected(value: D | null) { + this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); + } private _selected: D | null; /** The minimum selectable date. */ @Input() get minDate(): D | null { return this._minDate; } - set minDate(value: D | null) { this._minDate = this._dateAdapter.deserialize(value); } + set minDate(value: D | null) { + this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); + } private _minDate: D | null; /** The maximum selectable date. */ @Input() get maxDate(): D | null { return this._maxDate; } - set maxDate(value: D | null) { this._maxDate = this._dateAdapter.deserialize(value); } + set maxDate(value: D | null) { + this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); + } private _maxDate: D | null; /** A function used to filter which dates are selectable. */ @@ -384,4 +392,12 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges { (this._dateAdapter.getMonth(date) >= 7 ? 5 : 12); return this._dateAdapter.addCalendarMonths(date, increment); } + + /** + * @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-input.ts b/src/lib/datepicker/datepicker-input.ts index 7a30767003c4..45b847adc845 100644 --- a/src/lib/datepicker/datepicker-input.ts +++ b/src/lib/datepicker/datepicker-input.ts @@ -115,7 +115,6 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce value = this._dateAdapter.deserialize(value); this._lastValueValid = !value || this._dateAdapter.isValid(value); value = this._getValidDateOrNull(value); - let oldDate = this.value; this._value = value; this._renderer.setProperty(this._elementRef.nativeElement, 'value', @@ -130,7 +129,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get min(): D | null { return this._min; } set min(value: D | null) { - this._min = this._dateAdapter.deserialize(value); + this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); this._validatorOnChange(); } private _min: D | null; @@ -139,7 +138,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce @Input() get max(): D | null { return this._max; } set max(value: D | null) { - this._max = this._dateAdapter.deserialize(value); + this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); this._validatorOnChange(); } private _max: D | null; @@ -187,7 +186,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the min date. */ private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.deserialize(control.value); + const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value)); return (!this.min || !controlValue || this._dateAdapter.compareDate(this.min, controlValue) <= 0) ? null : {'matDatepickerMin': {'min': this.min, 'actual': controlValue}}; @@ -195,7 +194,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the max date. */ private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.deserialize(control.value); + const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value)); return (!this.max || !controlValue || this._dateAdapter.compareDate(this.max, controlValue) >= 0) ? null : {'matDatepickerMax': {'max': this.max, 'actual': controlValue}}; @@ -203,7 +202,7 @@ export class MatDatepickerInput implements AfterContentInit, ControlValueAcce /** The form control validator for the date filter. */ private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const controlValue = this._dateAdapter.deserialize(control.value); + const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value)); return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ? null : {'matDatepickerFilter': true}; } diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index ccd2fbfab3a5..4df9754f2284 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -272,13 +272,10 @@ describe('MatDatepicker', () => { expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar'); }); - it('should throw when given wrong data type', () => { + it('should not throw when given wrong data type', () => { testComponent.date = '1/1/2017' as any; - expect(() => fixture.detectChanges()) - .toThrowError('Could not deserialize "1/1/2017" into a valid date object.'); - - testComponent.date = null; + expect(() => fixture.detectChanges()).not.toThrow(); }); }); diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index 6a09edeb858c..a530e467dc0a 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -132,7 +132,9 @@ export class MatDatepicker implements OnDestroy { // selected value is. return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null); } - set startAt(date: D | null) { this._startAt = this._dateAdapter.deserialize(date); } + set startAt(date: D | null) { + this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(date)); + } private _startAt: D | null; /** The view that the calendar should start in. */ @@ -366,4 +368,12 @@ export class MatDatepicker implements OnDestroy { { overlayX: 'end', overlayY: 'bottom' } ); } + + /** + * @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/month-view.ts b/src/lib/datepicker/month-view.ts index 356db8365c0d..c8d9fc0ae926 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -46,7 +46,8 @@ export class MatMonthView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today(); + this._activeDate = + this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { this._init(); } @@ -57,7 +58,7 @@ export class MatMonthView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = this._dateAdapter.deserialize(value); + this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); this._selectedDate = this._getDateInCurrentMonth(this._selected); } private _selected: D | null; @@ -185,4 +186,12 @@ export class MatMonthView implements AfterContentInit { return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) && this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2)); } + + /** + * @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/year-view.ts b/src/lib/datepicker/year-view.ts index fd6cf6ac5b1c..3e60fdf190e9 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -41,7 +41,8 @@ export class MatYearView implements AfterContentInit { get activeDate(): D { return this._activeDate; } set activeDate(value: D) { let oldActiveDate = this._activeDate; - this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today(); + this._activeDate = + this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) { this._init(); } @@ -52,7 +53,7 @@ export class MatYearView implements AfterContentInit { @Input() get selected(): D | null { return this._selected; } set selected(value: D | null) { - this._selected = this._dateAdapter.deserialize(value); + this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); this._selectedMonth = this._getMonthInCurrentYear(this._selected); } private _selected: D | null; @@ -154,4 +155,12 @@ export class MatYearView implements AfterContentInit { return false; } + + /** + * @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/material-moment-adapter/adapter/moment-date-adapter.spec.ts b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts index ea3da86a9185..547f8db2ba26 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.spec.ts @@ -16,6 +16,7 @@ import {MomentDateAdapter} from './moment-date-adapter'; describe('MomentDateAdapter', () => { let adapter: MomentDateAdapter; + let assertValidDate: (d: moment.Moment | null, valid: boolean) => void; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -27,6 +28,13 @@ describe('MomentDateAdapter', () => { moment.locale('en'); adapter = d; adapter.setLocale('en'); + + assertValidDate = (d: moment.Moment | null, valid: boolean) => { + expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`); + expect(adapter.isValid(d!)).toBe(valid, + `Expected ${d} to be ${valid ? 'valid' : 'invalid'},` + + ` but was ${valid ? 'invalid' : 'valid'}`); + } })); it('should get year', () => { @@ -306,17 +314,17 @@ describe('MomentDateAdapter', () => { }); it('should create valid dates from valid ISO strings', () => { - expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull(); - expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull(); - expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull(); - expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow(); - expect(() => adapter.deserialize('1/1/2017')).toThrow(); + assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true); + assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true); + assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); + assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false); + assertValidDate(adapter.deserialize('1/1/2017'), false); expect(adapter.deserialize('')).toBeNull(); expect(adapter.deserialize(null)).toBeNull(); - expect(adapter.deserialize(new Date())).not.toBeNull(); - expect(() => adapter.deserialize(new Date(NaN))).toThrow(); - expect(adapter.deserialize(moment())).not.toBeNull(); - expect(() => adapter.deserialize(moment.invalid())).toThrow(); + assertValidDate(adapter.deserialize(new Date()), true); + assertValidDate(adapter.deserialize(new Date(NaN)), false); + assertValidDate(adapter.deserialize(moment()), true); + assertValidDate(adapter.deserialize(moment.invalid()), false); }); it('setLocale should not modify global moment locale', () => { @@ -357,6 +365,10 @@ describe('MomentDateAdapter', () => { adapter.isValid(date); expect(date.locale()).toBe('en'); }); + + it('should create invalid date', () => { + assertValidDate(adapter.invalid(), false); + }); }); describe('MomentDateAdapter with MAT_DATE_LOCALE override', () => { diff --git a/src/material-moment-adapter/adapter/moment-date-adapter.ts b/src/material-moment-adapter/adapter/moment-date-adapter.ts index 1d52c9006725..ec56d812881b 100644 --- a/src/material-moment-adapter/adapter/moment-date-adapter.ts +++ b/src/material-moment-adapter/adapter/moment-date-adapter.ts @@ -177,7 +177,7 @@ export class MomentDateAdapter extends DateAdapter { /** * Returns the given value if given a valid Moment or null. Deserializes valid ISO 8601 strings * (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects into valid Moments and empty - * string into null. Throws on all other values. + * string into null. Returns an invalid date for all other values. */ deserialize(value: any): Moment | null { let date; @@ -203,4 +203,8 @@ export class MomentDateAdapter extends DateAdapter { isValid(date: Moment): boolean { return this.clone(date).isValid(); } + + invalid(): Moment { + return moment.invalid(); + } }