Skip to content

feat(material/datepicker): add getValidDateOrNull method #19915

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

Merged
merged 1 commit into from
Jul 29, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ describe('MomentDateAdapter', () => {
expect(adapter.isDateInstance(d)).toBe(false);
});

it('should provide a method to return a valid date or null', () => {
let d = moment();
expect(adapter.getValidDateOrNull(d)).toBe(d);
expect(adapter.getValidDateOrNull(moment(NaN))).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);
Expand Down
10 changes: 10 additions & 0 deletions src/material/core/datetime/date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ export abstract class DateAdapter<D> {
*/
abstract invalid(): D;

/**
* Given a potential date object, returns that same date object if it is
* a valid date, or `null` if it's not a valid date.
* @param obj The object to check.
* @returns A date or `null`.
*/
getValidDateOrNull(obj: unknown): D | null {
return this.isDateInstance(obj) && this.isValid(obj as D) ? obj as D : null;
}

/**
* 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
Expand Down
12 changes: 12 additions & 0 deletions src/material/core/datetime/native-date-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ describe('NativeDateAdapter', () => {
expect(adapter.isDateInstance(d)).toBe(false);
});

it('should provide a method to return a valid date or null', () => {
let d = new Date();
expect(adapter.getValidDateOrNull(d)).toBe(d);
expect(adapter.getValidDateOrNull(new Date(NaN))).toBeNull();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably also add cases for null, undefined, '', 0, a date string, and a timestamp just to be comprehensive.

expect(adapter.getValidDateOrNull(null)).toBeNull();
expect(adapter.getValidDateOrNull(undefined)).toBeNull();
expect(adapter.getValidDateOrNull('')).toBeNull();
expect(adapter.getValidDateOrNull(0)).toBeNull();
expect(adapter.getValidDateOrNull('Wed Jul 28 1993')).toBeNull();
expect(adapter.getValidDateOrNull('1595204418000')).toBeNull();
});

it('should create 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);
Expand Down
16 changes: 4 additions & 12 deletions src/material/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
@Input()
get startAt(): D | null { return this._startAt; }
set startAt(value: D | null) {
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._startAt = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _startAt: D | null;

Expand All @@ -220,7 +220,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
if (value instanceof DateRange) {
this._selected = value;
} else {
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._selected = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
}
private _selected: DateRange<D> | D | null;
Expand All @@ -229,15 +229,15 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
@Input()
get minDate(): D | null { return this._minDate; }
set minDate(value: D | null) {
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._minDate = this._dateAdapter.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 = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._maxDate = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _maxDate: D | null;

Expand Down Expand Up @@ -417,14 +417,6 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
this.currentView = view;
}

/**
* @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;
}

/** Returns the component instance that corresponds to the current calendar view. */
private _getCurrentViewComponent() {
return this.monthView || this.yearView || this.multiYearView;
Expand Down
5 changes: 3 additions & 2 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ const _MatDateRangeInputBase:
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
/** Validator that checks that the start date isn't after the end date. */
private _startValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const start = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const start = this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(control.value));
const end = this._model ? this._model.selection.end : null;
return (!start || !end ||
this._dateAdapter.compareDate(start, end) <= 0) ?
Expand Down Expand Up @@ -280,7 +281,7 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
/** Validator that checks that the end date isn't before the start date. */
private _endValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const end = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const end = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const start = this._model ? this._model.selection.start : null;
return (!end || !start ||
this._dateAdapter.compareDate(end, start) >= 0) ?
Expand Down
12 changes: 2 additions & 10 deletions src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
@Input()
get min(): D | null { return this._min; }
set min(value: D | null) {
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._min = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
this._revalidate();
}
private _min: D | null;
Expand All @@ -137,7 +137,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
@Input()
get max(): D | null { return this._max; }
set max(value: D | null) {
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._max = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
this._revalidate();
}
private _max: D | null;
Expand Down Expand Up @@ -314,14 +314,6 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
return formField && formField._hasFloatingLabel() ? formField._labelId : null;
}

/**
* @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;
}

/** Re-runs the validators on the start/end inputs. */
private _revalidate() {
if (this._startInput) {
Expand Down
10 changes: 1 addition & 9 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
return this._startAt || (this._datepickerInput ? this._datepickerInput.getStartValue() : null);
}
set startAt(value: D | null) {
this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._startAt = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _startAt: D | null;

Expand Down Expand Up @@ -625,14 +625,6 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
]);
}

/**
* @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;
}

static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_touchUi: BooleanInput;
}
21 changes: 8 additions & 13 deletions src/material/datepicker/datepicker-input-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
set value(value: D | null) {
value = this._dateAdapter.deserialize(value);
this._lastValueValid = this._isValidValue(value);
value = this._getValidDateOrNull(value);
value = this._dateAdapter.getValidDateOrNull(value);
const oldDate = this.value;
this._assignValue(value);
this._formatValue(value);
Expand Down Expand Up @@ -144,15 +144,17 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection

/** The form control validator for the date filter. */
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const controlValue = this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(control.value));
const dateFilter = this._getDateFilter();
return !dateFilter || !controlValue || dateFilter(controlValue) ?
null : {'matDatepickerFilter': true};
}

/** The form control validator for the min date. */
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const controlValue = this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(control.value));
const min = this._getMinDate();
return (!min || !controlValue ||
this._dateAdapter.compareDate(min, controlValue) <= 0) ?
Expand All @@ -161,7 +163,8 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection

/** The form control validator for the max date. */
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const controlValue = this._getValidDateOrNull(this._dateAdapter.deserialize(control.value));
const controlValue = this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(control.value));
const max = this._getMaxDate();
return (!max || !controlValue ||
this._dateAdapter.compareDate(max, controlValue) >= 0) ?
Expand Down Expand Up @@ -300,7 +303,7 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
const lastValueWasValid = this._lastValueValid;
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
this._lastValueValid = this._isValidValue(date);
date = this._getValidDateOrNull(date);
date = this._dateAdapter.getValidDateOrNull(date);

if (!this._dateAdapter.sameDate(date, this.value)) {
this._assignValue(date);
Expand Down Expand Up @@ -340,14 +343,6 @@ export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection
value ? this._dateAdapter.format(value, this._dateFormats.display.dateInput) : '';
}

/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
*/
protected _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}

/** Assigns a value to the model. */
private _assignValue(value: D | null) {
// We may get some incoming values before the model was
Expand Down
4 changes: 2 additions & 2 deletions src/material/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
@Input()
get min(): D | null { return this._min; }
set min(value: D | null) {
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._min = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
this._validatorOnChange();
}
private _min: D | null;
Expand All @@ -92,7 +92,7 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D>
@Input()
get max(): D | null { return this._max; }
set max(value: D | null) {
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._max = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
this._validatorOnChange();
}
private _max: D | null;
Expand Down
18 changes: 6 additions & 12 deletions src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
set activeDate(value: D) {
const oldActiveDate = this._activeDate;
const validDate =
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(value)
) || this._dateAdapter.today();
this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate);
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
this._init();
Expand All @@ -91,7 +93,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
if (value instanceof DateRange) {
this._selected = value;
} else {
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._selected = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}

this._setRanges(this._selected);
Expand All @@ -102,15 +104,15 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
@Input()
get minDate(): D | null { return this._minDate; }
set minDate(value: D | null) {
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._minDate = this._dateAdapter.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 = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._maxDate = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _maxDate: D | null;

Expand Down Expand Up @@ -412,14 +414,6 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
return null;
}

/**
* @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;
}

/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
Expand Down
18 changes: 6 additions & 12 deletions src/material/datepicker/multi-year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
const validDate =
this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today();
this._dateAdapter.getValidDateOrNull(
this._dateAdapter.deserialize(value)
) || this._dateAdapter.today();
this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate);

if (!isSameMultiYearView(
Expand All @@ -80,7 +82,7 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
if (value instanceof DateRange) {
this._selected = value;
} else {
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._selected = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}

this._setSelectedYear(value);
Expand All @@ -92,15 +94,15 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
@Input()
get minDate(): D | null { return this._minDate; }
set minDate(value: D | null) {
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._minDate = this._dateAdapter.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 = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._maxDate = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value));
}
private _maxDate: D | null;

Expand Down Expand Up @@ -280,14 +282,6 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
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;
}

/** Determines whether the user has the RTL layout direction. */
private _isRtl() {
return this._dir && this._dir.value === 'rtl';
Expand Down
Loading