mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(datepicker): allow DateAdapter authors to have more control ove… (#7346)
* 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` * address comments * more comments addressed * return invalid date instead of throwing
This commit is contained in:
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -390,5 +407,4 @@ describe('NativeDateAdapter with LOCALE_ID override', () => {
|
||||
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
|
||||
import {first} from 'rxjs/operators';
|
||||
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';
|
||||
@@ -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. */
|
||||
@@ -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. */
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import {async, inject, TestBed} from '@angular/core/testing';
|
||||
import {DateAdapter, JAN, MatNativeDateModule} from '@angular/material/core';
|
||||
import {coerceDateProperty} from './index';
|
||||
|
||||
|
||||
describe('coerceDateProperty', () => {
|
||||
let adapter: DateAdapter<Date>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatNativeDateModule],
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([DateAdapter], (dateAdapter: DateAdapter<Date>) => {
|
||||
adapter = dateAdapter;
|
||||
}));
|
||||
|
||||
it('should pass through existing date', () => {
|
||||
const d = new Date(2017, JAN, 1);
|
||||
expect(coerceDateProperty(adapter, d)).toBe(d);
|
||||
});
|
||||
|
||||
it('should pass through invalid date', () => {
|
||||
const d = new Date(NaN);
|
||||
expect(coerceDateProperty(adapter, d)).toBe(d);
|
||||
});
|
||||
|
||||
it('should pass through null and undefined', () => {
|
||||
expect(coerceDateProperty(adapter, null)).toBeNull();
|
||||
expect(coerceDateProperty(adapter, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should coerce empty string to null', () => {
|
||||
expect(coerceDateProperty(adapter, '')).toBe(null);
|
||||
});
|
||||
|
||||
it('should coerce ISO 8601 string to date', () => {
|
||||
let isoString = '2017-01-01T00:00:00Z';
|
||||
expect(coerceDateProperty(adapter, isoString)).toEqual(new Date(isoString));
|
||||
});
|
||||
|
||||
it('should throw when given a number', () => {
|
||||
expect(() => coerceDateProperty(adapter, 5)).toThrow();
|
||||
expect(() => coerceDateProperty(adapter, 0)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when given a string with incorrect format', () => {
|
||||
expect(() => coerceDateProperty(adapter, '1/1/2017')).toThrow();
|
||||
expect(() => coerceDateProperty(adapter, 'hello')).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DateAdapter} from '@angular/material/core';
|
||||
|
||||
|
||||
/**
|
||||
* Function that attempts to coerce a value to a date using a DateAdapter. Date instances, null,
|
||||
* and undefined will be passed through. Empty strings will be coerced to null. Valid ISO 8601
|
||||
* strings (https://www.ietf.org/rfc/rfc3339.txt) will be coerced to dates. All other values will
|
||||
* result in an error being thrown.
|
||||
* @param adapter The date adapter to use for coercion
|
||||
* @param value The value to coerce.
|
||||
* @return A date object coerced from the value.
|
||||
* @throws Throws when the value cannot be coerced.
|
||||
*/
|
||||
export function coerceDateProperty<D>(adapter: DateAdapter<D>, value: any): D | null {
|
||||
if (typeof value === 'string') {
|
||||
if (value == '') {
|
||||
value = null;
|
||||
} else {
|
||||
value = adapter.fromIso8601(value) || value;
|
||||
}
|
||||
}
|
||||
if (value == null || adapter.isDateInstance(value)) {
|
||||
return value;
|
||||
}
|
||||
throw Error(`Datepicker: Value must be either a date object recognized by the DateAdapter or ` +
|
||||
`an ISO 8601 string. Instead got: ${value}`);
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
|
||||
import {MatFormField} from '@angular/material/form-field';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {coerceDateProperty} from './coerce-date-property';
|
||||
import {MatDatepicker} from './datepicker';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
|
||||
@@ -113,10 +112,9 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
return this._value;
|
||||
}
|
||||
set value(value: D | null) {
|
||||
value = coerceDateProperty(this._dateAdapter, value);
|
||||
value = this._dateAdapter.deserialize(value);
|
||||
this._lastValueValid = !value || this._dateAdapter.isValid(value);
|
||||
value = this._getValidDateOrNull(value);
|
||||
|
||||
let oldDate = this.value;
|
||||
this._value = value;
|
||||
this._renderer.setProperty(this._elementRef.nativeElement, 'value',
|
||||
@@ -131,7 +129,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
@Input()
|
||||
get min(): D | null { return this._min; }
|
||||
set min(value: D | null) {
|
||||
this._min = coerceDateProperty(this._dateAdapter, value);
|
||||
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
|
||||
this._validatorOnChange();
|
||||
}
|
||||
private _min: D | null;
|
||||
@@ -140,7 +138,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
@Input()
|
||||
get max(): D | null { return this._max; }
|
||||
set max(value: D | null) {
|
||||
this._max = coerceDateProperty(this._dateAdapter, value);
|
||||
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
|
||||
this._validatorOnChange();
|
||||
}
|
||||
private _max: D | null;
|
||||
@@ -188,7 +186,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
|
||||
/** The form control validator for the min date. */
|
||||
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
|
||||
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
|
||||
return (!this.min || !controlValue ||
|
||||
this._dateAdapter.compareDate(this.min, controlValue) <= 0) ?
|
||||
null : {'matDatepickerMin': {'min': this.min, 'actual': controlValue}};
|
||||
@@ -196,7 +194,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
|
||||
/** The form control validator for the max date. */
|
||||
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
|
||||
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
|
||||
return (!this.max || !controlValue ||
|
||||
this._dateAdapter.compareDate(this.max, controlValue) >= 0) ?
|
||||
null : {'matDatepickerMax': {'max': this.max, 'actual': controlValue}};
|
||||
@@ -204,7 +202,7 @@ export class MatDatepickerInput<D> implements AfterContentInit, ControlValueAcce
|
||||
|
||||
/** The form control validator for the date filter. */
|
||||
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
|
||||
const controlValue = coerceDateProperty(this._dateAdapter, control.value);
|
||||
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
|
||||
return !this._dateFilter || !controlValue || this._dateFilter(controlValue) ?
|
||||
null : {'matDatepickerFilter': true};
|
||||
}
|
||||
|
||||
@@ -272,14 +272,10 @@ describe('MatDatepicker', () => {
|
||||
expect((ownedElement as Element).tagName.toLowerCase()).toBe('mat-calendar');
|
||||
});
|
||||
|
||||
it('should throw when given wrong data type', () => {
|
||||
it('should not throw when given wrong data type', () => {
|
||||
testComponent.date = '1/1/2017' as any;
|
||||
|
||||
expect(() => fixture.detectChanges()).toThrowError(
|
||||
'Datepicker: Value must be either a date object recognized by the DateAdapter or an ' +
|
||||
'ISO 8601 string. Instead got: 1/1/2017');
|
||||
|
||||
testComponent.date = null;
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ import {DOCUMENT} from '@angular/platform-browser';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {MatCalendar} from './calendar';
|
||||
import {coerceDateProperty} from './coerce-date-property';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MatDatepickerInput} from './datepicker-input';
|
||||
|
||||
@@ -133,7 +132,9 @@ export class MatDatepicker<D> implements OnDestroy {
|
||||
// selected value is.
|
||||
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
|
||||
}
|
||||
set startAt(date: D | null) { this._startAt = coerceDateProperty(this._dateAdapter, date); }
|
||||
set startAt(date: D | null) {
|
||||
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(date));
|
||||
}
|
||||
private _startAt: D | null;
|
||||
|
||||
/** The view that the calendar should start in. */
|
||||
@@ -367,4 +368,12 @@ export class MatDatepicker<D> implements OnDestroy {
|
||||
{ overlayX: 'end', overlayY: 'bottom' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
|
||||
import {MatCalendarCell} from './calendar-body';
|
||||
import {coerceDateProperty} from './coerce-date-property';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
|
||||
|
||||
@@ -47,7 +46,8 @@ export class MatMonthView<D> implements AfterContentInit {
|
||||
get activeDate(): D { return this._activeDate; }
|
||||
set activeDate(value: D) {
|
||||
let oldActiveDate = this._activeDate;
|
||||
this._activeDate = coerceDateProperty(this._dateAdapter, value) || this._dateAdapter.today();
|
||||
this._activeDate =
|
||||
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
|
||||
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
|
||||
this._init();
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class MatMonthView<D> implements AfterContentInit {
|
||||
@Input()
|
||||
get selected(): D | null { return this._selected; }
|
||||
set selected(value: D | null) {
|
||||
this._selected = coerceDateProperty(this._dateAdapter, value);
|
||||
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
|
||||
this._selectedDate = this._getDateInCurrentMonth(this._selected);
|
||||
}
|
||||
private _selected: D | null;
|
||||
@@ -186,4 +186,12 @@ export class MatMonthView<D> implements AfterContentInit {
|
||||
return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) &&
|
||||
this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
export * from './datepicker-module';
|
||||
export * from './calendar';
|
||||
export * from './calendar-body';
|
||||
export * from './coerce-date-property';
|
||||
export * from './datepicker';
|
||||
export * from './datepicker-input';
|
||||
export * from './datepicker-intl';
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
|
||||
import {MatCalendarCell} from './calendar-body';
|
||||
import {coerceDateProperty} from './coerce-date-property';
|
||||
import {createMissingDateImplError} from './datepicker-errors';
|
||||
|
||||
|
||||
@@ -42,7 +41,8 @@ export class MatYearView<D> implements AfterContentInit {
|
||||
get activeDate(): D { return this._activeDate; }
|
||||
set activeDate(value: D) {
|
||||
let oldActiveDate = this._activeDate;
|
||||
this._activeDate = coerceDateProperty(this._dateAdapter, value) || this._dateAdapter.today();
|
||||
this._activeDate =
|
||||
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
|
||||
if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) {
|
||||
this._init();
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export class MatYearView<D> implements AfterContentInit {
|
||||
@Input()
|
||||
get selected(): D | null { return this._selected; }
|
||||
set selected(value: D | null) {
|
||||
this._selected = coerceDateProperty(this._dateAdapter, value);
|
||||
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
|
||||
this._selectedMonth = this._getMonthInCurrentYear(this._selected);
|
||||
}
|
||||
private _selected: D | null;
|
||||
@@ -155,4 +155,12 @@ export class MatYearView<D> implements AfterContentInit {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MomentDateAdapter} from './moment-date-adapter';
|
||||
import {async, inject, TestBed} from '@angular/core/testing';
|
||||
import {MomentDateModule} from './index';
|
||||
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
|
||||
import {LOCALE_ID} from '@angular/core';
|
||||
import {async, inject, TestBed} from '@angular/core/testing';
|
||||
import {DateAdapter, DEC, FEB, JAN, MAR, MAT_DATE_LOCALE} from '@angular/material/core';
|
||||
import * as moment from 'moment';
|
||||
|
||||
|
||||
// Month constants for more readable tests.
|
||||
const JAN = 0, FEB = 1, MAR = 2, DEC = 11;
|
||||
import {MomentDateModule} from './index';
|
||||
import {MomentDateAdapter} from './moment-date-adapter';
|
||||
|
||||
|
||||
describe('MomentDateAdapter', () => {
|
||||
let adapter: MomentDateAdapter;
|
||||
let assertValidDate: (d: moment.Moment | null, valid: boolean) => void;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -31,6 +28,13 @@ describe('MomentDateAdapter', () => {
|
||||
moment.locale('en');
|
||||
adapter = d;
|
||||
adapter.setLocale('en');
|
||||
|
||||
assertValidDate = (d: moment.Moment | 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', () => {
|
||||
@@ -309,12 +313,18 @@ describe('MomentDateAdapter', () => {
|
||||
expect(adapter.isDateInstance(d)).toBe(false);
|
||||
});
|
||||
|
||||
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('1990-13-31T23:59:00Z')).toBeNull();
|
||||
expect(adapter.fromIso8601('1/1/2017')).toBeNull();
|
||||
it('should create valid dates from valid ISO strings', () => {
|
||||
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('1990-13-31T23:59:00Z'), false);
|
||||
assertValidDate(adapter.deserialize('1/1/2017'), false);
|
||||
expect(adapter.deserialize('')).toBeNull();
|
||||
expect(adapter.deserialize(null)).toBeNull();
|
||||
assertValidDate(adapter.deserialize(new Date()), true);
|
||||
assertValidDate(adapter.deserialize(new Date(NaN)), false);
|
||||
assertValidDate(adapter.deserialize(moment()), true);
|
||||
assertValidDate(adapter.deserialize(moment.invalid()), false);
|
||||
});
|
||||
|
||||
it('setLocale should not modify global moment locale', () => {
|
||||
@@ -355,6 +365,10 @@ describe('MomentDateAdapter', () => {
|
||||
adapter.isValid(date);
|
||||
expect(date.locale()).toBe('en');
|
||||
});
|
||||
|
||||
it('should create invalid date', () => {
|
||||
assertValidDate(adapter.invalid(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MomentDateAdapter with MAT_DATE_LOCALE override', () => {
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
import {Inject, Injectable, Optional} from '@angular/core';
|
||||
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
|
||||
|
||||
// Depending on whether rollup is used, moment needs to be imported differently.
|
||||
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
|
||||
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
|
||||
// the `default as` syntax.
|
||||
// TODO(mmalerba): See if we can clean this up at some point.
|
||||
import {default as _rollupMoment, Moment} from 'moment';
|
||||
import * as _moment from 'moment';
|
||||
import {default as _rollupMoment, Moment} from 'moment';
|
||||
|
||||
const moment = _rollupMoment || _moment;
|
||||
|
||||
|
||||
@@ -174,9 +174,26 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
|
||||
return this.clone(date).format();
|
||||
}
|
||||
|
||||
fromIso8601(iso8601String: string): Moment | null {
|
||||
let d = moment(iso8601String, moment.ISO_8601).locale(this.locale);
|
||||
return this.isValid(d) ? d : null;
|
||||
/**
|
||||
* Returns the given value if given a valid Moment or null. Deserializes valid ISO 8601 strings
|
||||
* (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects into valid Moments and empty
|
||||
* string into null. Returns an invalid date for all other values.
|
||||
*/
|
||||
deserialize(value: any): Moment | null {
|
||||
let date;
|
||||
if (value instanceof Date) {
|
||||
date = moment(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
date = moment(value, moment.ISO_8601).locale(this.locale);
|
||||
}
|
||||
if (date && this.isValid(date)) {
|
||||
return date;
|
||||
}
|
||||
return super.deserialize(value);
|
||||
}
|
||||
|
||||
isDateInstance(obj: any): boolean {
|
||||
@@ -186,4 +203,8 @@ export class MomentDateAdapter extends DateAdapter<Moment> {
|
||||
isValid(date: Moment): boolean {
|
||||
return this.clone(date).isValid();
|
||||
}
|
||||
|
||||
invalid(): Moment {
|
||||
return moment.invalid();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user