Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(datepicker): allow DateAdapter authors to have more control ove… #7346

Merged
merged 4 commits into from
Oct 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions src/lib/core/datetime/date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export abstract class DateAdapter<D> {
abstract today(): D;

/**
* Parses a date from a 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).
Expand All @@ -127,7 +127,7 @@ export abstract class DateAdapter<D> {
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.
Expand Down Expand Up @@ -165,18 +165,13 @@ export abstract class DateAdapter<D> {

/**
* 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 `<input>`.
* @param date The date to get the ISO date string for.
* @returns The ISO date string date string.
*/
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
Expand All @@ -191,6 +186,31 @@ export abstract class DateAdapter<D> {
*/
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
* 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 `<mat-datepicker>` will call this
* method on all of it's `@Input()` properties that accept dates. It is therefore possible to
* 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), or an invalid date.
*/
deserialize(value: any): D | null {
if (value == null || this.isDateInstance(value) && this.isValid(value)) {
return value;
}
return this.invalid();
}

/**
* Sets the locale used for all dates.
* @param locale The new locale.
Expand Down Expand Up @@ -221,7 +241,15 @@ export abstract class DateAdapter<D> {
* 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;
}

/**
Expand Down
34 changes: 25 additions & 9 deletions src/lib/core/datetime/native-date-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from

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({
Expand All @@ -18,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', () => {
Expand Down Expand Up @@ -333,14 +342,22 @@ 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();
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();
assertValidDate(adapter.deserialize(new Date()), true);
assertValidDate(adapter.deserialize(new Date(NaN)), false);
});

it('should create an invalid date', () => {
assertValidDate(adapter.invalid(), false);
});
});

Expand Down Expand Up @@ -390,5 +407,4 @@ describe('NativeDateAdapter with LOCALE_ID override', () => {

expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue);
});

});
33 changes: 24 additions & 9 deletions src/lib/core/datetime/native-date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -219,16 +220,26 @@ export class NativeDateAdapter extends DateAdapter<Date> {
].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. Deserializes valid ISO 8601 strings
* (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') {
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.deserialize(value);
}

isDateInstance(obj: any) {
Expand All @@ -239,6 +250,10 @@ export class NativeDateAdapter extends DateAdapter<Date> {
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);
Expand Down
25 changes: 20 additions & 5 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,7 +66,9 @@ export class MatCalendar<D> 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._getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _startAt: D | null;

/** Whether the calendar should be started in month or year view. */
Expand All @@ -76,19 +77,25 @@ export class MatCalendar<D> 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._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 = coerceDateProperty(this._dateAdapter, 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 = coerceDateProperty(this._dateAdapter, 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. */
Expand Down Expand Up @@ -385,4 +392,12 @@ export class MatCalendar<D> 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;
}
}
54 changes: 0 additions & 54 deletions src/lib/datepicker/coerce-date-property.spec.ts

This file was deleted.

35 changes: 0 additions & 35 deletions src/lib/datepicker/coerce-date-property.ts

This file was deleted.

Loading