Skip to content

Commit a4585d6

Browse files
committed
return invalid date instead of throwing
1 parent a31aec4 commit a4585d6

File tree

11 files changed

+131
-46
lines changed

11 files changed

+131
-46
lines changed

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ export abstract class DateAdapter<D> {
186186
*/
187187
abstract isValid(date: D): boolean;
188188

189+
/**
190+
* Gets date instance that is not valid.
191+
* @returns An invalid date.
192+
*/
193+
abstract invalid(): D;
194+
189195
/**
190196
* Attempts to deserialize a value to a valid date object. This is different from parsing in that
191197
* deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601
@@ -196,14 +202,13 @@ export abstract class DateAdapter<D> {
196202
* to also deserialize the format used by your backend.
197203
* @param value The value to be deserialized into a date object.
198204
* @returns The deserialized date object, either a valid date, null if the value can be
199-
* deserialized into a null date (e.g. the empty string).
200-
* @throws If the given value cannot be deserialized into a valid date or null.
205+
* deserialized into a null date (e.g. the empty string), or an invalid date.
201206
*/
202207
deserialize(value: any): D | null {
203208
if (value == null || this.isDateInstance(value) && this.isValid(value)) {
204209
return value;
205210
}
206-
throw Error(`Could not deserialize "${value}" into a valid date object.`);
211+
return this.invalid();
207212
}
208213

209214
/**
@@ -236,7 +241,15 @@ export abstract class DateAdapter<D> {
236241
* Null dates are considered equal to other null dates.
237242
*/
238243
sameDate(first: D | null, second: D | null): boolean {
239-
return first && second ? !this.compareDate(first, second) : first == second;
244+
if (first && second) {
245+
let firstValid = this.isValid(first);
246+
let secondValid = this.isValid(second);
247+
if (firstValid && secondValid) {
248+
return !this.compareDate(first, second)
249+
}
250+
return firstValid == secondValid;
251+
}
252+
return first == second;
240253
}
241254

242255
/**

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const SUPPORTS_INTL = typeof Intl != 'undefined';
1010
describe('NativeDateAdapter', () => {
1111
const platform = new Platform();
1212
let adapter: NativeDateAdapter;
13+
let assertValidDate: (d: Date | null, valid: boolean) => void;
1314

1415
beforeEach(async(() => {
1516
TestBed.configureTestingModule({
@@ -19,6 +20,13 @@ describe('NativeDateAdapter', () => {
1920

2021
beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => {
2122
adapter = d;
23+
24+
assertValidDate = (d: Date | null, valid: boolean) => {
25+
expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`);
26+
expect(adapter.isValid(d!)).toBe(valid,
27+
`Expected ${d} to be ${valid ? 'valid' : 'invalid'},` +
28+
` but was ${valid ? 'invalid' : 'valid'}`);
29+
}
2230
}));
2331

2432
it('should get year', () => {
@@ -334,18 +342,22 @@ describe('NativeDateAdapter', () => {
334342
});
335343

336344
it('should create dates from valid ISO strings', () => {
337-
expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull();
338-
expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull();
339-
expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
340-
expect(adapter.deserialize('2017-01-01')).not.toBeNull();
341-
expect(adapter.deserialize('2017-01-01T00:00:00')).not.toBeNull();
342-
expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow();
343-
expect(() => adapter.deserialize('1/1/2017')).toThrow();
344-
expect(() => adapter.deserialize('2017-01-01T')).toThrow();
345+
assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true);
346+
assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true);
347+
assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true);
348+
assertValidDate(adapter.deserialize('2017-01-01'), true);
349+
assertValidDate(adapter.deserialize('2017-01-01T00:00:00'), true);
350+
assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false);
351+
assertValidDate(adapter.deserialize('1/1/2017'), false);
352+
assertValidDate(adapter.deserialize('2017-01-01T'), false);
345353
expect(adapter.deserialize('')).toBeNull();
346354
expect(adapter.deserialize(null)).toBeNull();
347-
expect(adapter.deserialize(new Date())).not.toBeNull();
348-
expect(() => adapter.deserialize(new Date(NaN))).toThrow();
355+
assertValidDate(adapter.deserialize(new Date()), true);
356+
assertValidDate(adapter.deserialize(new Date(NaN)), false);
357+
});
358+
359+
it('should create an invalid date', () => {
360+
assertValidDate(adapter.invalid(), false);
349361
});
350362
});
351363

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ export class NativeDateAdapter extends DateAdapter<Date> {
222222

223223
/**
224224
* Returns the given value if given a valid Date or null. Deserializes valid ISO 8601 strings
225-
* (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Throws on
226-
* all other values.
225+
* (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Returns an
226+
* invalid date for all other values.
227227
*/
228228
deserialize(value: any): Date | null {
229229
if (typeof value === 'string') {
@@ -250,6 +250,10 @@ export class NativeDateAdapter extends DateAdapter<Date> {
250250
return !isNaN(date.getTime());
251251
}
252252

253+
invalid(): Date {
254+
return new Date(NaN);
255+
}
256+
253257
/** Creates a date but allows the month and date to overflow. */
254258
private _createDateWithOverflow(year: number, month: number, date: number) {
255259
let result = new Date(year, month, date);

src/lib/datepicker/calendar.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
6666
/** A date representing the period (month or year) to start the calendar in. */
6767
@Input()
6868
get startAt(): D | null { return this._startAt; }
69-
set startAt(value: D | null) { this._startAt = this._dateAdapter.deserialize(value); }
69+
set startAt(value: D | null) {
70+
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
71+
}
7072
private _startAt: D | null;
7173

7274
/** Whether the calendar should be started in month or year view. */
@@ -75,19 +77,25 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
7577
/** The currently selected date. */
7678
@Input()
7779
get selected(): D | null { return this._selected; }
78-
set selected(value: D | null) { this._selected = this._dateAdapter.deserialize(value); }
80+
set selected(value: D | null) {
81+
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
82+
}
7983
private _selected: D | null;
8084

8185
/** The minimum selectable date. */
8286
@Input()
8387
get minDate(): D | null { return this._minDate; }
84-
set minDate(value: D | null) { this._minDate = this._dateAdapter.deserialize(value); }
88+
set minDate(value: D | null) {
89+
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
90+
}
8591
private _minDate: D | null;
8692

8793
/** The maximum selectable date. */
8894
@Input()
8995
get maxDate(): D | null { return this._maxDate; }
90-
set maxDate(value: D | null) { this._maxDate = this._dateAdapter.deserialize(value); }
96+
set maxDate(value: D | null) {
97+
this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
98+
}
9199
private _maxDate: D | null;
92100

93101
/** A function used to filter which dates are selectable. */
@@ -384,4 +392,12 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
384392
(this._dateAdapter.getMonth(date) >= 7 ? 5 : 12);
385393
return this._dateAdapter.addCalendarMonths(date, increment);
386394
}
395+
396+
/**
397+
* @param obj The object to check.
398+
* @returns The given object if it is both a date instance and valid, otherwise null.
399+
*/
400+
private _getValidDateOrNull(obj: any): D | null {
401+
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
402+
}
387403
}

src/lib/datepicker/datepicker-input.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
115115
value = this._dateAdapter.deserialize(value);
116116
this._lastValueValid = !value || this._dateAdapter.isValid(value);
117117
value = this._getValidDateOrNull(value);
118-
119118
let oldDate = this.value;
120119
this._value = value;
121120
this._renderer.setProperty(this._elementRef.nativeElement, 'value',
@@ -130,7 +129,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
130129
@Input()
131130
get min(): D | null { return this._min; }
132131
set min(value: D | null) {
133-
this._min = this._dateAdapter.deserialize(value);
132+
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
134133
this._validatorOnChange();
135134
}
136135
private _min: D | null;
@@ -139,7 +138,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
139138
@Input()
140139
get max(): D | null { return this._max; }
141140
set max(value: D | null) {
142-
this._max = this._dateAdapter.deserialize(value);
141+
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
143142
this._validatorOnChange();
144143
}
145144
private _max: D | null;
@@ -187,23 +186,23 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
187186

188187
/** The form control validator for the min date. */
189188
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
190-
const controlValue = this._dateAdapter.deserialize(control.value);
189+
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
191190
return (!this.min || !controlValue ||
192191
this._dateAdapter.compareDate(this.min, controlValue) <= 0) ?
193192
null : {'matDatepickerMin': {'min': this.min, 'actual': controlValue}};
194193
}
195194

196195
/** The form control validator for the max date. */
197196
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
198-
const controlValue = this._dateAdapter.deserialize(control.value);
197+
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
199198
return (!this.max || !controlValue ||
200199
this._dateAdapter.compareDate(this.max, controlValue) >= 0) ?
201200
null : {'matDatepickerMax': {'max': this.max, 'actual': controlValue}};
202201
}
203202

204203
/** The form control validator for the date filter. */
205204
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
206-
const controlValue = this._dateAdapter.deserialize(control.value);
205+
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
207206
return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ?
208207
null : {'matDatepickerFilter': true};
209208
}

src/lib/datepicker/datepicker.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,10 @@ describe('MatDatepicker', () => {
272272
expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
273273
});
274274

275-
it('should throw when given wrong data type', () => {
275+
it('should not throw when given wrong data type', () => {
276276
testComponent.date = '1/1/2017' as any;
277277

278-
expect(() => fixture.detectChanges())
279-
.toThrowError('Could not deserialize "1/1/2017" into a valid date object.');
280-
281-
testComponent.date = null;
278+
expect(() => fixture.detectChanges()).not.toThrow();
282279
});
283280
});
284281

src/lib/datepicker/datepicker.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ export class MatDatepicker<D> implements OnDestroy {
132132
// selected value is.
133133
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
134134
}
135-
set startAt(date: D | null) { this._startAt = this._dateAdapter.deserialize(date); }
135+
set startAt(date: D | null) {
136+
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(date));
137+
}
136138
private _startAt: D | null;
137139

138140
/** The view that the calendar should start in. */
@@ -366,4 +368,12 @@ export class MatDatepicker<D> implements OnDestroy {
366368
{ overlayX: 'end', overlayY: 'bottom' }
367369
);
368370
}
371+
372+
/**
373+
* @param obj The object to check.
374+
* @returns The given object if it is both a date instance and valid, otherwise null.
375+
*/
376+
private _getValidDateOrNull(obj: any): D | null {
377+
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
378+
}
369379
}

src/lib/datepicker/month-view.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export class MatMonthView<D> implements AfterContentInit {
4646
get activeDate(): D { return this._activeDate; }
4747
set activeDate(value: D) {
4848
let oldActiveDate = this._activeDate;
49-
this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today();
49+
this._activeDate =
50+
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
5051
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
5152
this._init();
5253
}
@@ -57,7 +58,7 @@ export class MatMonthView<D> implements AfterContentInit {
5758
@Input()
5859
get selected(): D | null { return this._selected; }
5960
set selected(value: D | null) {
60-
this._selected = this._dateAdapter.deserialize(value);
61+
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
6162
this._selectedDate = this._getDateInCurrentMonth(this._selected);
6263
}
6364
private _selected: D | null;
@@ -185,4 +186,12 @@ export class MatMonthView<D> implements AfterContentInit {
185186
return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) &&
186187
this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2));
187188
}
189+
190+
/**
191+
* @param obj The object to check.
192+
* @returns The given object if it is both a date instance and valid, otherwise null.
193+
*/
194+
private _getValidDateOrNull(obj: any): D | null {
195+
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
196+
}
188197
}

src/lib/datepicker/year-view.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export class MatYearView<D> implements AfterContentInit {
4141
get activeDate(): D { return this._activeDate; }
4242
set activeDate(value: D) {
4343
let oldActiveDate = this._activeDate;
44-
this._activeDate = this._dateAdapter.deserialize(value) || this._dateAdapter.today();
44+
this._activeDate =
45+
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
4546
if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) {
4647
this._init();
4748
}
@@ -52,7 +53,7 @@ export class MatYearView<D> implements AfterContentInit {
5253
@Input()
5354
get selected(): D | null { return this._selected; }
5455
set selected(value: D | null) {
55-
this._selected = this._dateAdapter.deserialize(value);
56+
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
5657
this._selectedMonth = this._getMonthInCurrentYear(this._selected);
5758
}
5859
private _selected: D | null;
@@ -154,4 +155,12 @@ export class MatYearView<D> implements AfterContentInit {
154155

155156
return false;
156157
}
158+
159+
/**
160+
* @param obj The object to check.
161+
* @returns The given object if it is both a date instance and valid, otherwise null.
162+
*/
163+
private _getValidDateOrNull(obj: any): D | null {
164+
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
165+
}
157166
}

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {MomentDateAdapter} from './moment-date-adapter';
1616

1717
describe('MomentDateAdapter', () => {
1818
let adapter: MomentDateAdapter;
19+
let assertValidDate: (d: moment.Moment | null, valid: boolean) => void;
1920

2021
beforeEach(async(() => {
2122
TestBed.configureTestingModule({
@@ -27,6 +28,13 @@ describe('MomentDateAdapter', () => {
2728
moment.locale('en');
2829
adapter = d;
2930
adapter.setLocale('en');
31+
32+
assertValidDate = (d: moment.Moment | null, valid: boolean) => {
33+
expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`);
34+
expect(adapter.isValid(d!)).toBe(valid,
35+
`Expected ${d} to be ${valid ? 'valid' : 'invalid'},` +
36+
` but was ${valid ? 'invalid' : 'valid'}`);
37+
}
3038
}));
3139

3240
it('should get year', () => {
@@ -306,17 +314,17 @@ describe('MomentDateAdapter', () => {
306314
});
307315

308316
it('should create valid dates from valid ISO strings', () => {
309-
expect(adapter.deserialize('1985-04-12T23:20:50.52Z')).not.toBeNull();
310-
expect(adapter.deserialize('1996-12-19T16:39:57-08:00')).not.toBeNull();
311-
expect(adapter.deserialize('1937-01-01T12:00:27.87+00:20')).not.toBeNull();
312-
expect(() => adapter.deserialize('1990-13-31T23:59:00Z')).toThrow();
313-
expect(() => adapter.deserialize('1/1/2017')).toThrow();
317+
assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true);
318+
assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true);
319+
assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true);
320+
assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false);
321+
assertValidDate(adapter.deserialize('1/1/2017'), false);
314322
expect(adapter.deserialize('')).toBeNull();
315323
expect(adapter.deserialize(null)).toBeNull();
316-
expect(adapter.deserialize(new Date())).not.toBeNull();
317-
expect(() => adapter.deserialize(new Date(NaN))).toThrow();
318-
expect(adapter.deserialize(moment())).not.toBeNull();
319-
expect(() => adapter.deserialize(moment.invalid())).toThrow();
324+
assertValidDate(adapter.deserialize(new Date()), true);
325+
assertValidDate(adapter.deserialize(new Date(NaN)), false);
326+
assertValidDate(adapter.deserialize(moment()), true);
327+
assertValidDate(adapter.deserialize(moment.invalid()), false);
320328
});
321329

322330
it('setLocale should not modify global moment locale', () => {
@@ -357,6 +365,10 @@ describe('MomentDateAdapter', () => {
357365
adapter.isValid(date);
358366
expect(date.locale()).toBe('en');
359367
});
368+
369+
it('should create invalid date', () => {
370+
assertValidDate(adapter.invalid(), false);
371+
});
360372
});
361373

362374
describe('MomentDateAdapter with MAT_DATE_LOCALE override', () => {

0 commit comments

Comments
 (0)