Skip to content

Commit 027be57

Browse files
committed
WIP: coerceDateProperty
1 parent 8e668cb commit 027be57

14 files changed

+150
-115
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,14 @@ export abstract class DateAdapter<D> {
169169
* @param date The date to get the ISO date string for.
170170
* @returns The ISO date string date string.
171171
*/
172-
abstract toISODateString(date: D): string;
172+
abstract toIsoDateString(date: D): string;
173173

174174
/**
175175
* Creates a date from an RFC 3339 compatible date string (https://tools.ietf.org/html/rfc3339).
176176
* @param iso8601String The ISO date string to create a date from
177177
* @returns The date created from the ISO date string.
178178
*/
179-
abstract fromISODateString(iso8601String: string): D | null;
179+
abstract fromIsoDateString(iso8601String: string): D | null;
180180

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

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,11 @@ describe('NativeDateAdapter', () => {
333333
});
334334

335335
it('should create dates from valid ISO strings', () => {
336-
expect(adapter.fromISODateString('1985-04-12T23:20:50.52Z')).not.toBeNull();
337-
expect(adapter.fromISODateString('1996-12-19T16:39:57-08:00')).not.toBeNull();
338-
expect(adapter.fromISODateString('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
339-
expect(adapter.fromISODateString('1990-13-31T23:59:00Z')).toBeNull();
340-
expect(adapter.fromISODateString('1/1/2017')).toBeNull();
336+
expect(adapter.fromIsoDateString('1985-04-12T23:20:50.52Z')).not.toBeNull();
337+
expect(adapter.fromIsoDateString('1996-12-19T16:39:57-08:00')).not.toBeNull();
338+
expect(adapter.fromIsoDateString('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
339+
expect(adapter.fromIsoDateString('1990-13-31T23:59:00Z')).toBeNull();
340+
expect(adapter.fromIsoDateString('1/1/2017')).toBeNull();
341341
});
342342
});
343343

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,17 @@ export class NativeDateAdapter extends DateAdapter<Date> {
211211
this.getYear(date), this.getMonth(date), this.getDate(date) + days);
212212
}
213213

214-
toISODateString(date: Date): string {
214+
toIsoDateString(date: Date): string {
215215
return [
216216
date.getUTCFullYear(),
217217
this._2digit(date.getUTCMonth() + 1),
218218
this._2digit(date.getUTCDate())
219219
].join('-');
220220
}
221221

222-
fromISODateString(iso8601String: string): Date | null {
222+
fromIsoDateString(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.
223225
if (iso8601String.match(ISO_8601_REGEX)) {
224226
let d = new Date(iso8601String);
225227
if (this.isValid(d)) {

src/lib/datepicker/calendar.ts

+7-18
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ 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';
4344
import {createMissingDateImplError} from './datepicker-errors';
4445
import {MdDatepickerIntl} from './datepicker-intl';
4546

@@ -64,29 +65,29 @@ export class MdCalendar<D> implements AfterContentInit, OnDestroy {
6465

6566
/** A date representing the period (month or year) to start the calendar in. */
6667
@Input()
67-
get startAt(): D { return this._startAt; }
68-
set startAt(value: D) { this._startAt = this._coerceDateProperty(value); }
69-
private _startAt: D;
68+
get startAt(): D | null { return this._startAt; }
69+
set startAt(value: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, value); }
70+
private _startAt: D | null;
7071

7172
/** Whether the calendar should be started in month or year view. */
7273
@Input() startView: 'month' | 'year' = 'month';
7374

7475
/** The currently selected date. */
7576
@Input()
7677
get selected(): D | null { return this._selected; }
77-
set selected(value: D | null) { this._selected = this._coerceDateProperty(value); }
78+
set selected(value: D | null) { this._selected = coerceDateProperty(this._dateAdapter, value); }
7879
private _selected: D | null;
7980

8081
/** The minimum selectable date. */
8182
@Input()
8283
get minDate(): D | null { return this._minDate; }
83-
set minDate(value: D | null) { this._minDate = this._coerceDateProperty(value); }
84+
set minDate(value: D | null) { this._minDate = coerceDateProperty(this._dateAdapter, value); }
8485
private _minDate: D | null;
8586

8687
/** The maximum selectable date. */
8788
@Input()
8889
get maxDate(): D | null { return this._maxDate; }
89-
set maxDate(value: D | null) { this._maxDate = this._coerceDateProperty(value); }
90+
set maxDate(value: D | null) { this._maxDate = coerceDateProperty(this._dateAdapter, value); }
9091
private _maxDate: D | null;
9192

9293
/** A function used to filter which dates are selectable. */
@@ -366,16 +367,4 @@ export class MdCalendar<D> implements AfterContentInit, OnDestroy {
366367
(this._dateAdapter.getMonth(date) >= 7 ? 5 : 12);
367368
return this._dateAdapter.addCalendarMonths(date, increment);
368369
}
369-
370-
/**
371-
* Attempts to coerce a property to a date by parsing it as a ISO 8601 string. If not a valid
372-
* ISO 8601 string, returns the original vlaue.
373-
*/
374-
private _coerceDateProperty(value: any): any {
375-
if (typeof value === 'string') {
376-
const d = this._dateAdapter.fromISODateString(value);
377-
return d || value;
378-
}
379-
return value;
380-
}
381370
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {async, inject, TestBed} from '@angular/core/testing';
2+
import {DateAdapter, JAN, MdNativeDateModule} from '@angular/material/core';
3+
import {coerceDateProperty} from '@angular/material/datepicker';
4+
5+
6+
describe('coerceDateProperty', () => {
7+
let adapter: DateAdapter<Date>;
8+
9+
beforeEach(async(() => {
10+
TestBed.configureTestingModule({
11+
imports: [MdNativeDateModule],
12+
});
13+
14+
TestBed.compileComponents();
15+
}));
16+
17+
beforeEach(inject([DateAdapter], (dateAdapter: DateAdapter<Date>) => {
18+
adapter = dateAdapter;
19+
}));
20+
21+
it('should pass through existing date', () => {
22+
const d = new Date(2017, JAN, 1);
23+
expect(coerceDateProperty(adapter, d)).toBe(d);
24+
});
25+
26+
it('should pass through invalid date', () => {
27+
const d = new Date(NaN);
28+
expect(coerceDateProperty(adapter, d)).toBe(d);
29+
});
30+
31+
it('should pass through null and undefined', () => {
32+
expect(coerceDateProperty(adapter, null)).toBeNull();
33+
expect(coerceDateProperty(adapter, undefined)).toBeUndefined();
34+
});
35+
36+
it('should coerce empty string to null', () => {
37+
expect(coerceDateProperty(adapter, '')).toBe(null);
38+
});
39+
40+
it('should coerce ISO 8601 string to date', () => {
41+
let isoString = '2017-01-01T00:00:00Z';
42+
expect(coerceDateProperty(adapter, isoString)).toEqual(new Date(isoString));
43+
});
44+
45+
it('should throw when given a number', () => {
46+
expect(() => coerceDateProperty(adapter, 5)).toThrow();
47+
expect(() => coerceDateProperty(adapter, 0)).toThrow();
48+
});
49+
50+
it('should throw when given a string with incorrect format', () => {
51+
expect(() => coerceDateProperty(adapter, '1/1/2017')).toThrow();
52+
expect(() => coerceDateProperty(adapter, 'hello')).toThrow();
53+
});
54+
});
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {DateAdapter} from '@angular/material/core';
10+
11+
12+
/**
13+
* Function that attempts to coerce a value to a date using a DateAdapter. Date instances, null,
14+
* and undefined will be passed through. Empty strings will be coerced to null. Valid ISO 8601
15+
* strings (https://www.ietf.org/rfc/rfc3339.txt) will be coerced to dates. All other values will
16+
* result in an error being thrown.
17+
* @param adapter The date adapter to use for coercion
18+
* @param value The value to coerce.
19+
* @return A date object coerced from the value.
20+
* @throws Throws when the value cannot be coerced.
21+
*/
22+
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.fromIsoDateString(value) || value;
28+
}
29+
}
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}`);
35+
}

src/lib/datepicker/datepicker-input.ts

+11-25
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ 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';
3738
import {MdDatepicker} from './datepicker';
3839
import {createMissingDateImplError} from './datepicker-errors';
3940

@@ -74,8 +75,8 @@ export class MdDatepickerInputEvent<D> {
7475
host: {
7576
'[attr.aria-haspopup]': 'true',
7677
'[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
77-
'[attr.min]': 'min ? _dateAdapter.toISODateString(min) : null',
78-
'[attr.max]': 'max ? _dateAdapter.toISODateString(max) : null',
78+
'[attr.min]': 'min ? _dateAdapter.toIsoDateString(min) : null',
79+
'[attr.max]': 'max ? _dateAdapter.toIsoDateString(max) : null',
7980
'[disabled]': 'disabled',
8081
'(input)': '_onInput($event.target.value)',
8182
'(change)': '_onChange()',
@@ -122,10 +123,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
122123
return this._value;
123124
}
124125
set value(value: D | null) {
125-
value = this._coerceDateProperty(value);
126-
if (value != null && !this._dateAdapter.isDateInstance(value)) {
127-
throw Error('Datepicker: value not recognized as a date object by DateAdapter.');
128-
}
126+
value = coerceDateProperty(this._dateAdapter, value);
129127
this._lastValueValid = !value || this._dateAdapter.isValid(value);
130128
value = this._getValidDateOrNull(value);
131129

@@ -143,7 +141,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
143141
@Input()
144142
get min(): D | null { return this._min; }
145143
set min(value: D | null) {
146-
this._min = this._coerceDateProperty(value);
144+
this._min = coerceDateProperty(this._dateAdapter, value);
147145
this._validatorOnChange();
148146
}
149147
private _min: D | null;
@@ -152,7 +150,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
152150
@Input()
153151
get max(): D | null { return this._max; }
154152
set max(value: D | null) {
155-
this._max = this._coerceDateProperty(value);
153+
this._max = coerceDateProperty(this._dateAdapter, value);
156154
this._validatorOnChange();
157155
}
158156
private _max: D | null;
@@ -200,23 +198,23 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
200198

201199
/** The form control validator for the min date. */
202200
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
203-
const controlValue = this._coerceDateProperty(control.value);
201+
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
204202
return (!this.min || !controlValue ||
205203
this._dateAdapter.compareDate(this.min, controlValue) <= 0) ?
206-
null : {'mdDatepickerMin': {'min': this.min, 'actual': control.value}};
204+
null : {'mdDatepickerMin': {'min': this.min, 'actual': controlValue}};
207205
}
208206

209207
/** The form control validator for the max date. */
210208
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
211-
const controlValue = this._coerceDateProperty(control.value);
209+
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
212210
return (!this.max || !controlValue ||
213211
this._dateAdapter.compareDate(this.max, controlValue) >= 0) ?
214-
null : {'mdDatepickerMax': {'max': this.max, 'actual': control.value}};
212+
null : {'mdDatepickerMax': {'max': this.max, 'actual': controlValue}};
215213
}
216214

217215
/** The form control validator for the date filter. */
218216
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
219-
const controlValue = this._coerceDateProperty(control.value);
217+
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
220218
return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ?
221219
null : {'mdDatepickerFilter': true};
222220
}
@@ -332,16 +330,4 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
332330
private _getValidDateOrNull(obj: any): D | null {
333331
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
334332
}
335-
336-
/**
337-
* Attempts to coerce a property to a date by parsing it as a ISO 8601 string. If not a valid
338-
* ISO 8601 string, returns the original vlaue.
339-
*/
340-
private _coerceDateProperty(value: any): any {
341-
if (typeof value === 'string') {
342-
const d = this._dateAdapter.fromISODateString(value);
343-
return d || value;
344-
}
345-
return value;
346-
}
347333
}

src/lib/datepicker/datepicker.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('MdDatepicker', () => {
4949
DatepickerWithChangeAndInputEvents,
5050
DatepickerWithFilterAndValidation,
5151
DatepickerWithFormControl,
52+
DatepickerWithISOStrings,
5253
DatepickerWithMinAndMaxValidation,
5354
DatepickerWithNgModel,
5455
DatepickerWithStartAt,
@@ -272,8 +273,8 @@ describe('MdDatepicker', () => {
272273
it('should throw when given wrong data type', () => {
273274
testComponent.date = '1/1/2017' as any;
274275

275-
expect(() => fixture.detectChanges())
276-
.toThrowError(/Datepicker: value not recognized as a date object by DateAdapter\./);
276+
expect(() => fixture.detectChanges()).toThrowError(
277+
/Datepicker: Value must be either a date object recognized by the DateAdapter or an ISO 8601 string\. Instead got: 1\/1\/2017/);
277278

278279
testComponent.date = null;
279280
});

src/lib/datepicker/datepicker.ts

+2-13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ 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';
4546
import {createMissingDateImplError} from './datepicker-errors';
4647
import {MdDatepickerInput} from './datepicker-input';
4748

@@ -129,7 +130,7 @@ export class MdDatepicker<D> implements OnDestroy {
129130
// selected value is.
130131
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
131132
}
132-
set startAt(date: D | null) { this._startAt = this._coerceDateProperty(date); }
133+
set startAt(date: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, date); }
133134
private _startAt: D | null;
134135

135136
/** The view that the calendar should start in. */
@@ -360,16 +361,4 @@ export class MdDatepicker<D> implements OnDestroy {
360361
{ overlayX: 'end', overlayY: 'bottom' }
361362
);
362363
}
363-
364-
/**
365-
* Attempts to coerce a property to a date by parsing it as a ISO 8601 string. If not a valid
366-
* ISO 8601 string, returns the original vlaue.
367-
*/
368-
private _coerceDateProperty(value: any): any {
369-
if (typeof value === 'string') {
370-
const d = this._dateAdapter.fromISODateString(value);
371-
return d || value;
372-
}
373-
return value;
374-
}
375364
}

0 commit comments

Comments
 (0)