Skip to content

Commit

Permalink
feat(material/datepicker): add getValidDateOrNull method (#19915)
Browse files Browse the repository at this point in the history
Several components have identical implementations of a `_getValidDateOrNull` method. This PR reduces code duplication by adding a `getValidDateOrNull` method to the `DateAdapter` class for the components to use instead.
  • Loading branch information
swseverance committed Jul 29, 2020
1 parent 77b11f4 commit f0e56f0
Show file tree
Hide file tree
Showing 13 changed files with 67 additions and 84 deletions.
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
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
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();
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
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
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 @@ -284,7 +285,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
Expand Up @@ -129,7 +129,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 @@ -138,7 +138,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 @@ -319,14 +319,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
Expand Up @@ -253,7 +253,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 @@ -641,14 +641,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
Expand Up @@ -77,7 +77,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 @@ -149,15 +149,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 @@ -166,7 +168,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 @@ -318,7 +321,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 @@ -358,14 +361,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
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
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
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

0 comments on commit f0e56f0

Please sign in to comment.