Skip to content

Commit 083fab0

Browse files
committed
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`
1 parent 3571f68 commit 083fab0

File tree

7 files changed

+95
-52
lines changed

7 files changed

+95
-52
lines changed

src/lib/core/datetime/date-adapter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,13 @@ export abstract class DateAdapter<D> {
171171
abstract toIso8601(date: D): string;
172172

173173
/**
174-
* Creates a date from an RFC 3339 compatible string (https://tools.ietf.org/html/rfc3339).
175-
* @param iso8601String The ISO date string to create a date from
176-
* @returns The date created from the ISO date string.
174+
* Attempts to coerce a value to a date object (e.g. a ISO 8601 string).
175+
* @param value The value to be coerced to a date object.
176+
* @returns The coerced date object, either a valid date, null if the value can be coerced to a
177+
* null date (e.g. the empty string), or an invalid date to indicate that the value could not
178+
* be coerced.
177179
*/
178-
abstract fromIso8601(iso8601String: string): D | null;
180+
abstract coerceToDate(value: any): D | null;
179181

180182
/**
181183
* Checks whether the given object is considered a date instance by this DateAdapter.

src/lib/core/datetime/native-date-adapter.spec.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from
66

77
const SUPPORTS_INTL = typeof Intl != 'undefined';
88

9+
10+
function expectValid(date: Date | null, valid: boolean) {
11+
if (date != null) {
12+
expect(!isNaN(date.getTime())).toBe(valid,
13+
`expected date to be ${valid ? 'valid' : 'invalid'}, was ${valid ? 'invalid' : 'valid'}`);
14+
} else {
15+
fail(`expected ${valid ? 'valid' : 'invalid'} date, was null`);
16+
}
17+
}
18+
19+
920
describe('NativeDateAdapter', () => {
1021
const platform = new Platform();
1122
let adapter: NativeDateAdapter;
@@ -333,14 +344,15 @@ describe('NativeDateAdapter', () => {
333344
});
334345

335346
it('should create dates from valid ISO strings', () => {
336-
expect(adapter.fromIso8601('1985-04-12T23:20:50.52Z')).not.toBeNull();
337-
expect(adapter.fromIso8601('1996-12-19T16:39:57-08:00')).not.toBeNull();
338-
expect(adapter.fromIso8601('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
339-
expect(adapter.fromIso8601('2017-01-01')).not.toBeNull();
340-
expect(adapter.fromIso8601('2017-01-01T00:00:00')).not.toBeNull();
341-
expect(adapter.fromIso8601('1990-13-31T23:59:00Z')).toBeNull();
342-
expect(adapter.fromIso8601('1/1/2017')).toBeNull();
343-
expect(adapter.fromIso8601('2017-01-01T')).toBeNull();
347+
expectValid(adapter.coerceToDate('1985-04-12T23:20:50.52Z'), true);
348+
expectValid(adapter.coerceToDate('1996-12-19T16:39:57-08:00'), true);
349+
expectValid(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20'), true);
350+
expectValid(adapter.coerceToDate('2017-01-01'), true);
351+
expectValid(adapter.coerceToDate('2017-01-01T00:00:00'), true);
352+
expectValid(adapter.coerceToDate('1990-13-31T23:59:00Z'), false);
353+
expectValid(adapter.coerceToDate('1/1/2017'), false);
354+
expectValid(adapter.coerceToDate('2017-01-01T'), false);
355+
expect(adapter.coerceToDate('')).toBeNull();
344356
});
345357
});
346358

@@ -390,5 +402,4 @@ describe('NativeDateAdapter with LOCALE_ID override', () => {
390402

391403
expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue);
392404
});
393-
394405
});

src/lib/core/datetime/native-date-adapter.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
*/
88

99
import {Inject, Injectable, Optional} from '@angular/core';
10-
import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter';
1110
import {extendObject} from '../util/object-extend';
11+
import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter';
12+
1213

1314
// TODO(mmalerba): Remove when we no longer support safari 9.
1415
/** Whether the browser supports the Intl API. */
@@ -219,16 +220,25 @@ export class NativeDateAdapter extends DateAdapter<Date> {
219220
].join('-');
220221
}
221222

222-
fromIso8601(iso8601String: string): Date | null {
223-
// The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the
224-
// string is the right format first.
225-
if (ISO_8601_REGEX.test(iso8601String)) {
226-
let d = new Date(iso8601String);
227-
if (this.isValid(d)) {
228-
return d;
223+
/**
224+
* Coerces valid ISO 8601 strings (https://www.ietf.org/rfc/rfc3339.txt) to valid dates, empty
225+
* string to null, all other values to an invalid date.
226+
*/
227+
coerceToDate(value: any): Date | null {
228+
if (value == null || this.isDateInstance(value)) {
229+
return value;
230+
}
231+
if (typeof value === 'string') {
232+
if (!value) {
233+
return null;
234+
}
235+
// The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the
236+
// string is the right format first.
237+
if (ISO_8601_REGEX.test(value)) {
238+
return new Date(value);
229239
}
230240
}
231-
return null;
241+
return new Date(NaN);
232242
}
233243

234244
isDateInstance(obj: any) {

src/lib/datepicker/coerce-date-property.spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ describe('coerceDateProperty', () => {
2323
expect(coerceDateProperty(adapter, d)).toBe(d);
2424
});
2525

26-
it('should pass through invalid date', () => {
27-
const d = new Date(NaN);
28-
expect(coerceDateProperty(adapter, d)).toBe(d);
29-
});
30-
3126
it('should pass through null and undefined', () => {
3227
expect(coerceDateProperty(adapter, null)).toBeNull();
3328
expect(coerceDateProperty(adapter, undefined)).toBeUndefined();
@@ -51,4 +46,8 @@ describe('coerceDateProperty', () => {
5146
expect(() => coerceDateProperty(adapter, '1/1/2017')).toThrow();
5247
expect(() => coerceDateProperty(adapter, 'hello')).toThrow();
5348
});
49+
50+
it('should throw when given an invalid date', () => {
51+
expect(() => coerceDateProperty(adapter, new Date(NaN))).toThrow();
52+
});
5453
});

src/lib/datepicker/coerce-date-property.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,10 @@ import {DateAdapter} from '@angular/material/core';
2020
* @throws Throws when the value cannot be coerced.
2121
*/
2222
export function coerceDateProperty<D>(adapter: DateAdapter<D>, value: any): D | null {
23-
if (typeof value === 'string') {
24-
if (value == '') {
25-
value = null;
26-
} else {
27-
value = adapter.fromIso8601(value) || value;
28-
}
23+
const d = adapter.coerceToDate(value);
24+
if (adapter.isDateInstance(d) && !adapter.isValid(d as D)) {
25+
throw Error(`Datepicker: Value must be either a date object recognized by the DateAdapter or ` +
26+
`an ISO 8601 string. Instead got: ${value}`);
2927
}
30-
if (value == null || adapter.isDateInstance(value)) {
31-
return value;
32-
}
33-
throw Error(`Datepicker: Value must be either a date object recognized by the DateAdapter or ` +
34-
`an ISO 8601 string. Instead got: ${value}`);
28+
return d;
3529
}

src/material-moment-adapter/adapter/moment-date-adapter.spec.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {MomentDateAdapter} from './moment-date-adapter';
9+
import {LOCALE_ID} from '@angular/core';
1010
import {async, inject, TestBed} from '@angular/core/testing';
11-
import {MomentDateModule} from './index';
1211
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
13-
import {LOCALE_ID} from '@angular/core';
1412
import * as moment from 'moment';
13+
import {Moment} from 'moment';
14+
import {MomentDateModule} from './index';
15+
import {MomentDateAdapter} from './moment-date-adapter';
1516

1617

1718
// Month constants for more readable tests.
1819
const JAN = 0, FEB = 1, MAR = 2, DEC = 11;
1920

2021

22+
function expectValid(date: Moment | null, valid: boolean) {
23+
if (date != null) {
24+
expect(date.isValid()).toBe(valid,
25+
`expected date to be ${valid ? 'valid' : 'invalid'}, was ${valid ? 'invalid' : 'valid'}`);
26+
} else {
27+
fail(`expected ${valid ? 'valid' : 'invalid'} date, was null`);
28+
}
29+
}
30+
31+
2132
describe('MomentDateAdapter', () => {
2233
let adapter: MomentDateAdapter;
2334

@@ -309,12 +320,13 @@ describe('MomentDateAdapter', () => {
309320
expect(adapter.isDateInstance(d)).toBe(false);
310321
});
311322

312-
it('should create dates from valid ISO strings', () => {
313-
expect(adapter.fromIso8601('1985-04-12T23:20:50.52Z')).not.toBeNull();
314-
expect(adapter.fromIso8601('1996-12-19T16:39:57-08:00')).not.toBeNull();
315-
expect(adapter.fromIso8601('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
316-
expect(adapter.fromIso8601('1990-13-31T23:59:00Z')).toBeNull();
317-
expect(adapter.fromIso8601('1/1/2017')).toBeNull();
323+
it('should create valid dates from valid ISO strings', () => {
324+
expectValid(adapter.coerceToDate('1985-04-12T23:20:50.52Z'), true);
325+
expectValid(adapter.coerceToDate('1996-12-19T16:39:57-08:00'), true);
326+
expectValid(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20'), true);
327+
expectValid(adapter.coerceToDate('1990-13-31T23:59:00Z'), false);
328+
expectValid(adapter.coerceToDate('1/1/2017'), false);
329+
expect(adapter.coerceToDate('')).toBeNull();
318330
});
319331

320332
it('setLocale should not modify global moment locale', () => {

src/material-moment-adapter/adapter/moment-date-adapter.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88

99
import {Inject, Injectable, Optional} from '@angular/core';
1010
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
11-
1211
// Depending on whether rollup is used, moment needs to be imported differently.
1312
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
1413
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
1514
// the `default as` syntax.
1615
// TODO(mmalerba): See if we can clean this up at some point.
17-
import {default as _rollupMoment, Moment} from 'moment';
1816
import * as _moment from 'moment';
17+
import {default as _rollupMoment, Moment} from 'moment';
18+
1919
const moment = _rollupMoment || _moment;
2020

2121

@@ -174,9 +174,24 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
174174
return this.clone(date).format();
175175
}
176176

177-
fromIso8601(iso8601String: string): Moment | null {
178-
let d = moment(iso8601String, moment.ISO_8601).locale(this.locale);
179-
return this.isValid(d) ? d : null;
177+
/**
178+
* Coerces valid ISO 8601 strings (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects to
179+
* valid dates, empty string to null, all other values to an invalid date.
180+
*/
181+
coerceToDate(value: any): Moment | null {
182+
if (value == null || this.isDateInstance(value)) {
183+
return value;
184+
}
185+
if (value instanceof Date) {
186+
return moment(value);
187+
}
188+
if (typeof value === 'string') {
189+
if (!value) {
190+
return null;
191+
}
192+
return moment(value, moment.ISO_8601).locale(this.locale);
193+
}
194+
return moment.invalid();
180195
}
181196

182197
isDateInstance(obj: any): boolean {

0 commit comments

Comments
 (0)