Skip to content

Commit 1535594

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 1535594

14 files changed

+105
-157
lines changed

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,6 @@ export abstract class DateAdapter<D> {
170170
*/
171171
abstract toIso8601(date: D): string;
172172

173-
/**
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.
177-
*/
178-
abstract fromIso8601(iso8601String: string): D | null;
179-
180173
/**
181174
* Checks whether the given object is considered a date instance by this DateAdapter.
182175
* @param obj The object to check
@@ -191,6 +184,23 @@ export abstract class DateAdapter<D> {
191184
*/
192185
abstract isValid(date: D): boolean;
193186

187+
/**
188+
* Attempts to coerce a value to a valid date object. This is different from parsing in that it
189+
* should only coerce non-ambiguous, locale-independent values (e.g. a ISO 8601 string).
190+
* The default implementation does not allow any coercion, it simply checks that the given value
191+
* is already a valid date object or null.
192+
* @param value The value to be coerced to a date object.
193+
* @returns The coerced date object, either a valid date, null if the value can be coerced to a
194+
* null date (e.g. the empty string).
195+
* @throws If the given value cannot be coerced to a valid date or null.
196+
*/
197+
coerceToDate(value: any): D | null {
198+
if (value == null || this.isDateInstance(value) && this.isValid(value)) {
199+
return value;
200+
}
201+
throw Error(`Could not coerce "${value}" to a valid date object.`);
202+
}
203+
194204
/**
195205
* Sets the locale used for all dates.
196206
* @param locale The new locale.

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

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

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

9+
910
describe('NativeDateAdapter', () => {
1011
const platform = new Platform();
1112
let adapter: NativeDateAdapter;
@@ -333,14 +334,18 @@ describe('NativeDateAdapter', () => {
333334
});
334335

335336
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();
337+
expect(adapter.coerceToDate('1985-04-12T23:20:50.52Z')).not.toBeNull();
338+
expect(adapter.coerceToDate('1996-12-19T16:39:57-08:00')).not.toBeNull();
339+
expect(adapter.coerceToDate('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
340+
expect(adapter.coerceToDate('2017-01-01')).not.toBeNull();
341+
expect(adapter.coerceToDate('2017-01-01T00:00:00')).not.toBeNull();
342+
expect(() => adapter.coerceToDate('1990-13-31T23:59:00Z')).toThrow();
343+
expect(() => adapter.coerceToDate('1/1/2017')).toThrow();
344+
expect(() => adapter.coerceToDate('2017-01-01T')).toThrow();
345+
expect(adapter.coerceToDate('')).toBeNull();
346+
expect(adapter.coerceToDate(null)).toBeNull();
347+
expect(adapter.coerceToDate(new Date())).not.toBeNull();
348+
expect(() => adapter.coerceToDate(new Date(NaN))).toThrow();
344349
});
345350
});
346351

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

391396
expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue);
392397
});
393-
394398
});

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

Lines changed: 20 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,26 @@ 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+
* Returns the given value if given a valid Date or null. Coerces valid ISO 8601 strings
225+
* (https://www.ietf.org/rfc/rfc3339.txt) to valid Dates and empty string to null. Throws on all
226+
* other values.
227+
*/
228+
coerceToDate(value: any): Date | null {
229+
if (typeof value === 'string') {
230+
if (!value) {
231+
return null;
232+
}
233+
// The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the
234+
// string is the right format first.
235+
if (ISO_8601_REGEX.test(value)) {
236+
let date = new Date(value);
237+
if (this.isValid(date)) {
238+
return date;
239+
}
229240
}
230241
}
231-
return null;
242+
return super.coerceToDate(value);
232243
}
233244

234245
isDateInstance(obj: any) {

src/lib/datepicker/calendar.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
} from '@angular/material/core';
4141
import {first} from 'rxjs/operator/first';
4242
import {Subscription} from 'rxjs/Subscription';
43-
import {coerceDateProperty} from './coerce-date-property';
4443
import {createMissingDateImplError} from './datepicker-errors';
4544
import {MdDatepickerIntl} from './datepicker-intl';
4645

@@ -68,7 +67,7 @@ export class MdCalendar<D> implements AfterContentInit, OnDestroy {
6867
/** A date representing the period (month or year) to start the calendar in. */
6968
@Input()
7069
get startAt(): D | null { return this._startAt; }
71-
set startAt(value: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, value); }
70+
set startAt(value: D | null) { this._startAt = this._dateAdapter.coerceToDate(value); }
7271
private _startAt: D | null;
7372

7473
/** Whether the calendar should be started in month or year view. */
@@ -77,19 +76,19 @@ export class MdCalendar<D> implements AfterContentInit, OnDestroy {
7776
/** The currently selected date. */
7877
@Input()
7978
get selected(): D | null { return this._selected; }
80-
set selected(value: D | null) { this._selected = coerceDateProperty(this._dateAdapter, value); }
79+
set selected(value: D | null) { this._selected = this._dateAdapter.coerceToDate(value); }
8180
private _selected: D | null;
8281

8382
/** The minimum selectable date. */
8483
@Input()
8584
get minDate(): D | null { return this._minDate; }
86-
set minDate(value: D | null) { this._minDate = coerceDateProperty(this._dateAdapter, value); }
85+
set minDate(value: D | null) { this._minDate = this._dateAdapter.coerceToDate(value); }
8786
private _minDate: D | null;
8887

8988
/** The maximum selectable date. */
9089
@Input()
9190
get maxDate(): D | null { return this._maxDate; }
92-
set maxDate(value: D | null) { this._maxDate = coerceDateProperty(this._dateAdapter, value); }
91+
set maxDate(value: D | null) { this._maxDate = this._dateAdapter.coerceToDate(value); }
9392
private _maxDate: D | null;
9493

9594
/** A function used to filter which dates are selectable. */

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

Lines changed: 0 additions & 54 deletions
This file was deleted.

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

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/lib/datepicker/datepicker-input.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
import {DateAdapter, MD_DATE_FORMATS, MdDateFormats} from '@angular/material/core';
3535
import {MdFormField} from '@angular/material/form-field';
3636
import {Subscription} from 'rxjs/Subscription';
37-
import {coerceDateProperty} from './coerce-date-property';
3837
import {MdDatepicker} from './datepicker';
3938
import {createMissingDateImplError} from './datepicker-errors';
4039

@@ -123,7 +122,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
123122
return this._value;
124123
}
125124
set value(value: D | null) {
126-
value = coerceDateProperty(this._dateAdapter, value);
125+
value = this._dateAdapter.coerceToDate(value);
127126
this._lastValueValid = !value || this._dateAdapter.isValid(value);
128127
value = this._getValidDateOrNull(value);
129128

@@ -141,7 +140,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
141140
@Input()
142141
get min(): D | null { return this._min; }
143142
set min(value: D | null) {
144-
this._min = coerceDateProperty(this._dateAdapter, value);
143+
this._min = this._dateAdapter.coerceToDate(value);
145144
this._validatorOnChange();
146145
}
147146
private _min: D | null;
@@ -150,7 +149,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
150149
@Input()
151150
get max(): D | null { return this._max; }
152151
set max(value: D | null) {
153-
this._max = coerceDateProperty(this._dateAdapter, value);
152+
this._max = this._dateAdapter.coerceToDate(value);
154153
this._validatorOnChange();
155154
}
156155
private _max: D | null;
@@ -198,23 +197,23 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
198197

199198
/** The form control validator for the min date. */
200199
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
201-
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
200+
const controlValue = this._dateAdapter.coerceToDate(control.value);
202201
return (!this.min || !controlValue ||
203202
this._dateAdapter.compareDate(this.min, controlValue) <= 0) ?
204203
null : {'mdDatepickerMin': {'min': this.min, 'actual': controlValue}};
205204
}
206205

207206
/** The form control validator for the max date. */
208207
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
209-
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
208+
const controlValue = this._dateAdapter.coerceToDate(control.value);
210209
return (!this.max || !controlValue ||
211210
this._dateAdapter.compareDate(this.max, controlValue) >= 0) ?
212211
null : {'mdDatepickerMax': {'max': this.max, 'actual': controlValue}};
213212
}
214213

215214
/** The form control validator for the date filter. */
216215
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
217-
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
216+
const controlValue = this._dateAdapter.coerceToDate(control.value);
218217
return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ?
219218
null : {'mdDatepickerFilter': true};
220219
}

src/lib/datepicker/datepicker.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,8 @@ describe('MdDatepicker', () => {
273273
it('should throw when given wrong data type', () => {
274274
testComponent.date = '1/1/2017' as any;
275275

276-
expect(() => fixture.detectChanges()).toThrowError(
277-
'Datepicker: Value must be either a date object recognized by the DateAdapter or an ' +
278-
'ISO 8601 string. Instead got: 1/1/2017');
276+
expect(() => fixture.detectChanges())
277+
.toThrowError('Could not coerce "1/1/2017" to a valid date object.');
279278

280279
testComponent.date = null;
281280
});

src/lib/datepicker/datepicker.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import {DOCUMENT} from '@angular/platform-browser';
4242
import {Subject} from 'rxjs/Subject';
4343
import {Subscription} from 'rxjs/Subscription';
4444
import {MdCalendar} from './calendar';
45-
import {coerceDateProperty} from './coerce-date-property';
4645
import {createMissingDateImplError} from './datepicker-errors';
4746
import {MdDatepickerInput} from './datepicker-input';
4847

@@ -133,7 +132,7 @@ export class MdDatepicker<D> implements OnDestroy {
133132
// selected value is.
134133
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
135134
}
136-
set startAt(date: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, date); }
135+
set startAt(date: D | null) { this._startAt = this._dateAdapter.coerceToDate(date); }
137136
private _startAt: D | null;
138137

139138
/** The view that the calendar should start in. */

src/lib/datepicker/month-view.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
MdDateFormats
2323
} from '@angular/material/core';
2424
import {MdCalendarCell} from './calendar-body';
25-
import {coerceDateProperty} from './coerce-date-property';
2625
import {createMissingDateImplError} from './datepicker-errors';
2726

2827

@@ -50,7 +49,7 @@ export class MdMonthView<D> implements AfterContentInit {
5049
get activeDate(): D { return this._activeDate; }
5150
set activeDate(value: D) {
5251
let oldActiveDate = this._activeDate;
53-
this._activeDate = coerceDateProperty(this._dateAdapter, value) || this._dateAdapter.today();
52+
this._activeDate = this._dateAdapter.coerceToDate(value) || this._dateAdapter.today();
5453
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
5554
this._init();
5655
}
@@ -61,7 +60,7 @@ export class MdMonthView<D> implements AfterContentInit {
6160
@Input()
6261
get selected(): D | null { return this._selected; }
6362
set selected(value: D | null) {
64-
this._selected = coerceDateProperty(this._dateAdapter, value);
63+
this._selected = this._dateAdapter.coerceToDate(value);
6564
this._selectedDate = this._getDateInCurrentMonth(this._selected);
6665
}
6766
private _selected: D | null;

src/lib/datepicker/public_api.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
export * from './datepicker-module';
1010
export * from './calendar';
1111
export * from './calendar-body';
12-
export * from './coerce-date-property';
1312
export * from './datepicker';
1413
export * from './datepicker-input';
1514
export * from './datepicker-intl';

0 commit comments

Comments
 (0)