diff --git a/src/material/core/datetime/date-selection-model.ts b/src/material/core/datetime/date-selection-model.ts index 60dce0276a5d..f5c8731fa476 100644 --- a/src/material/core/datetime/date-selection-model.ts +++ b/src/material/core/datetime/date-selection-model.ts @@ -6,151 +6,173 @@ * found in the LICENSE file at https://angular.io/license */ +import {FactoryProvider, Injectable, OnDestroy, Optional, SkipSelf} from '@angular/core'; import {DateAdapter} from '@angular/material/core'; -import {FactoryProvider, Optional, SkipSelf, Injectable} from '@angular/core'; -import {Subject, Observable} from 'rxjs'; +import {Observable, Subject} from 'rxjs'; -export abstract class MatDateSelectionModel { - protected _valueChangesSubject = new Subject(); +/** A class representing a range of dates. */ +export class DateRange { + /** + * Ensures that objects with a `start` and `end` property can't be assigned to a variable that + * expects a `DateRange` + */ + // tslint:disable-next-line:no-unused-variable + private _disableStructuralEquivalency: never; + + /** The start date of the range. */ + readonly start: D | null; + + /** The end date of the range. */ + readonly end: D | null; + + constructor(range?: {start?: D | null, end?: D | null} | null) { + this.start = range && range.start || null; + this.end = range && range.end || null; + } +} + +type ExtractDateTypeFromSelection = T extends DateRange ? D : NonNullable; + +/** A selection model containing a date selection. */ +export abstract class MatDateSelectionModel> + implements OnDestroy { + /** Subject used to emit value change events. */ + private _valueChangesSubject = new Subject(); + + /** Observable of value change events. */ valueChanges: Observable = this._valueChangesSubject.asObservable(); - constructor(protected readonly adapter: DateAdapter) {} + /** The current selection. */ + get selection (): S { return this._selection; } + set selection(s: S) { + this._selection = s; + this._valueChangesSubject.next(); + } - destroy() { + protected constructor(protected readonly adapter: DateAdapter, private _selection: S) {} + + ngOnDestroy() { this._valueChangesSubject.complete(); } + /** Adds a date to the current selection. */ abstract add(date: D | null): void; + + /** Checks whether the current selection is complete. */ abstract isComplete(): boolean; + + /** + * Checks whether the current selection is the same as the selection in the given selection model. + */ abstract isSame(other: MatDateSelectionModel): boolean; + + /** Checks whether the current selection is valid. */ abstract isValid(): boolean; - abstract overlaps(range: DateRange): boolean; -} -export interface DateRange { - start: D | null; - end: D | null; + /** Checks whether the current selection overlaps with the given range. */ + abstract overlaps(range: DateRange): boolean; } -/** - * Concrete implementation of a MatDateSelectionModel that holds a single date. - */ +/** A selection model that contains a single date. */ @Injectable() -export class MatSingleDateSelectionModel extends MatDateSelectionModel { - private _date: D | null = null; - +export class MatSingleDateSelectionModel extends MatDateSelectionModel { constructor(adapter: DateAdapter, date?: D | null) { - super(adapter); - this._date = date === undefined ? null : date; + super(adapter, date || null); } + /** + * Adds a date to the current selection. In the case of a single date selection, the added date + * simply overwrites the previous selection + */ add(date: D | null) { - this._date = date; - this._valueChangesSubject.next(); + this.selection = date; } - compareDate(other: MatSingleDateSelectionModel) { - const date = this.asDate(); - const otherDate = other.asDate(); - if (date != null && otherDate != null) { - return this.adapter.compareDate(date, otherDate); - } - return date === otherDate; - } - - isComplete() { return this._date != null; } + /** + * Checks whether the current selection is complete. In the case of a single date selection, this + * is true if the current selection is not null. + */ + isComplete() { return this.selection != null; } - isSame(other: MatDateSelectionModel): boolean { + /** + * Checks whether the current selection is the same as the selection in the given selection model. + */ + isSame(other: MatDateSelectionModel): boolean { return other instanceof MatSingleDateSelectionModel && - this.adapter.sameDate(other.asDate(), this._date); + this.adapter.sameDate(other.selection, this.selection); } + /** + * Checks whether the current selection is valid. In the case of a single date selection, this + * means that the current selection is not null and is a valid date. + */ isValid(): boolean { - return this._date != null && this.adapter.isDateInstance(this._date) && - this.adapter.isValid(this._date); + return this.selection != null && this.adapter.isDateInstance(this.selection) && + this.adapter.isValid(this.selection); } - asDate(): D | null { - return this.isValid() ? this._date : null; - } - - setDate(date: D | null) { - this._date = date; - this._valueChangesSubject.next(); - } - - /** - * Determines if the single date is within a given date range. Retuns false if either dates of - * the range is null or if the selection is undefined. - */ + /** Checks whether the current selection overlaps with the given range. */ overlaps(range: DateRange): boolean { - return !!(this._date && range.start && range.end && - this.adapter.compareDate(range.start, this._date) <= 0 && - this.adapter.compareDate(this._date, range.end) <= 0); + return !!(this.selection && range.start && range.end && + this.adapter.compareDate(range.start, this.selection) <= 0 && + this.adapter.compareDate(this.selection, range.end) <= 0); } } -/** - * Concrete implementation of a MatDateSelectionModel that holds a date range, represented by - * a start date and an end date. - */ +/** A selection model that contains a date range. */ @Injectable() -export class MatRangeDateSelectionModel extends MatDateSelectionModel { - private _start: D | null = null; - private _end: D | null = null; - - constructor(adapter: DateAdapter, start?: D | null, end?: D | null) { - super(adapter); - this._start = start === undefined ? null : start; - this._end = end === undefined ? null : end; +export class MatRangeDateSelectionModel extends MatDateSelectionModel, D> { + constructor(adapter: DateAdapter, range?: {start?: D | null, end?: D | null} | null) { + super(adapter, new DateRange(range)); } /** - * Adds an additional date to the range. If no date is set thus far, it will set it to the - * beginning. If the beginning is set, it will set it to the end. - * If add is called on a complete selection, it will empty the selection and set it as the start. + * Adds a date to the current selection. In the case of a date range selection, the added date + * fills in the next `null` value in the range. If both the start and the end already have a date, + * the selection is reset so that the given date is the new `start` and the `end` is null. */ add(date: D | null): void { - if (this._start == null) { - this._start = date; - } else if (this._end == null) { - this._end = date; + let {start, end} = this.selection; + + if (start == null) { + start = date; + } else if (end == null) { + end = date; } else { - this._start = date; - this._end = null; + start = date; + end = null; } - this._valueChangesSubject.next(); - } - - setRange(start: D | null, end: D | null) { - this._start = start; - this._end = end; + this.selection = new DateRange({start, end}); } + /** + * Checks whether the current selection is complete. In the case of a date range selection, this + * is true if the current selection has a non-null `start` and `end`. + */ isComplete(): boolean { - return this._start != null && this._end != null; + return this.selection.start != null && this.selection.end != null; } - isSame(other: MatDateSelectionModel): boolean { + /** + * Checks whether the current selection is the same as the selection in the given selection model. + */ + isSame(other: MatDateSelectionModel): boolean { if (other instanceof MatRangeDateSelectionModel) { - const otherRange = other.asRange(); - return this.adapter.sameDate(this._start, otherRange.start) && - this.adapter.sameDate(this._end, otherRange.end); + return this.adapter.sameDate(this.selection.start, other.selection.start) && + this.adapter.sameDate(this.selection.end, other.selection.end); } return false; } + /** + * Checks whether the current selection is valid. In the case of a date range selection, this + * means that the current selection has a `start` and `end` that are both non-null and valid + * dates. + */ isValid(): boolean { - return this._start != null && this._end != null && - this.adapter.isValid(this._start!) && this.adapter.isValid(this._end!); - } - - asRange(): DateRange { - return { - start: this._start, - end: this._end, - }; + return this.selection.start != null && this.selection.end != null && + this.adapter.isValid(this.selection.start!) && this.adapter.isValid(this.selection.end!); } /** @@ -158,17 +180,17 @@ export class MatRangeDateSelectionModel extends MatDateSelectionModel { * includes incomplete selections or ranges. */ overlaps(range: DateRange): boolean { - if (!(this._start && this._end && range.start && range.end)) { + if (!(this.selection.start && this.selection.end && range.start && range.end)) { return false; } return ( - this._isBetween(range.start, this._start, this._end) || - this._isBetween(range.end, this._start, this._end) || - ( - this.adapter.compareDate(range.start, this._start) <= 0 && - this.adapter.compareDate(this._end, range.end) <= 0 - ) + this._isBetween(range.start, this.selection.start, this.selection.end) || + this._isBetween(range.end, this.selection.start, this.selection.end) || + ( + this.adapter.compareDate(range.start, this.selection.start) <= 0 && + this.adapter.compareDate(this.selection.end, range.end) <= 0 + ) ); } @@ -177,9 +199,8 @@ export class MatRangeDateSelectionModel extends MatDateSelectionModel { } } -export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(parent: - MatSingleDateSelectionModel, - adapter: DateAdapter) { +export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY( + parent: MatSingleDateSelectionModel, adapter: DateAdapter) { return parent || new MatSingleDateSelectionModel(adapter); } diff --git a/src/material/datepicker/datepicker-input.ts b/src/material/datepicker/datepicker-input.ts index 920c22c72302..74c1b48283f0 100644 --- a/src/material/datepicker/datepicker-input.ts +++ b/src/material/datepicker/datepicker-input.ts @@ -32,8 +32,8 @@ import { import { DateAdapter, MAT_DATE_FORMATS, - MatSingleDateSelectionModel, MatDateFormats, + MatSingleDateSelectionModel, ThemePalette, } from '@angular/material/core'; import {MatFormField} from '@angular/material/form-field'; @@ -67,10 +67,10 @@ export class MatDatepickerInputEvent { value: D | null; constructor( - /** Reference to the datepicker input component that emitted the event. */ - public target: MatDatepickerInput, - /** Reference to the native input element associated with the datepicker input. */ - public targetElement: HTMLElement) { + /** Reference to the datepicker input component that emitted the event. */ + public target: MatDatepickerInput, + /** Reference to the native input element associated with the datepicker input. */ + public targetElement: HTMLElement) { this.value = this.target.value; } } @@ -129,22 +129,20 @@ export class MatDatepickerInput implements ControlValueAccessor, OnDestroy, V /** The value of the input. */ @Input() - get value(): D | null { return this._selection.asDate(); } + get value(): D | null { return this._selectionModel.selection; } set value(value: D | null) { - value = this._dateAdapter.deserialize(value); - const oldDate = this._selection.asDate(); - const isDifferent = !this._dateAdapter.sameDate(oldDate, value); - this._selection.setDate(value); - - this._lastValueValid = this._selection.isValid(); - - this._formatValue(this._selection.asDate()); - - if (isDifferent) { - this._valueChange.emit(this.value); + value = this._dateAdapter.deserialize(value); + this._lastValueValid = !value || this._dateAdapter.isValid(value); + value = this._getValidDateOrNull(value); + const oldDate = this._selectionModel.selection; + this._selectionModel.selection = value; + this._formatValue(value); + + if (!this._dateAdapter.sameDate(oldDate, value)) { + this._valueChange.emit(value); } } - private _selection: MatSingleDateSelectionModel; + private _selectionModel: MatSingleDateSelectionModel; /** The minimum valid date. */ @Input() @@ -259,7 +257,7 @@ export class MatDatepickerInput implements ControlValueAccessor, OnDestroy, V throw createMissingDateImplError('MAT_DATE_FORMATS'); } - this._selection = new MatSingleDateSelectionModel(this._dateAdapter, null); + this._selectionModel = new MatSingleDateSelectionModel(this._dateAdapter); // Update the displayed date when the locale changes. this._localeSubscription = _dateAdapter.localeChanges.subscribe(() => { @@ -334,8 +332,8 @@ export class MatDatepickerInput implements ControlValueAccessor, OnDestroy, V this._lastValueValid = !date || this._dateAdapter.isValid(date); date = this._getValidDateOrNull(date); - if (!this._dateAdapter.sameDate(date, this._selection.asDate())) { - this._selection.setDate(date); + if (!this._dateAdapter.sameDate(date, this._selectionModel.selection)) { + this._selectionModel.selection = date; this._cvaOnChange(date); this._valueChange.emit(date); this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement)); diff --git a/tools/public_api_guard/material/core.d.ts b/tools/public_api_guard/material/core.d.ts index c753ea451c32..c76c21087c6f 100644 --- a/tools/public_api_guard/material/core.d.ts +++ b/tools/public_api_guard/material/core.d.ts @@ -79,9 +79,13 @@ export declare abstract class DateAdapter { abstract today(): D; } -export interface DateRange { - end: D | null; - start: D | null; +export declare class DateRange { + readonly end: D | null; + readonly start: D | null; + constructor(range?: { + start?: D | null; + end?: D | null; + } | null); } export declare const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9, NOV = 10, DEC = 11; @@ -205,7 +209,7 @@ export declare const MAT_OPTION_PARENT_COMPONENT: InjectionToken; -export declare function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(parent: MatSingleDateSelectionModel, adapter: DateAdapter): MatSingleDateSelectionModel; +export declare function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(parent: MatSingleDateSelectionModel, adapter: DateAdapter): MatSingleDateSelectionModel; export declare const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider; @@ -227,17 +231,19 @@ export declare type MatDateFormats = { }; }; -export declare abstract class MatDateSelectionModel { - protected _valueChangesSubject: Subject; +export declare abstract class MatDateSelectionModel> implements OnDestroy { protected readonly adapter: DateAdapter; + selection: S; valueChanges: Observable; - constructor(adapter: DateAdapter); + protected constructor(adapter: DateAdapter, _selection: S); abstract add(date: D | null): void; - destroy(): void; abstract isComplete(): boolean; abstract isSame(other: MatDateSelectionModel): boolean; abstract isValid(): boolean; + ngOnDestroy(): void; abstract overlaps(range: DateRange): boolean; + static ɵdir: i0.ɵɵDirectiveDefWithMeta, never, never, {}, {}, never>; + static ɵfac: i0.ɵɵFactoryDef>; } export declare const MATERIAL_SANITY_CHECKS: InjectionToken; @@ -334,15 +340,16 @@ export declare class MatPseudoCheckboxModule { export declare type MatPseudoCheckboxState = 'unchecked' | 'checked' | 'indeterminate'; -export declare class MatRangeDateSelectionModel extends MatDateSelectionModel { - constructor(adapter: DateAdapter, start?: D | null, end?: D | null); +export declare class MatRangeDateSelectionModel extends MatDateSelectionModel, D> { + constructor(adapter: DateAdapter, range?: { + start?: D | null; + end?: D | null; + } | null); add(date: D | null): void; - asRange(): DateRange; isComplete(): boolean; - isSame(other: MatDateSelectionModel): boolean; + isSame(other: MatDateSelectionModel): boolean; isValid(): boolean; overlaps(range: DateRange): boolean; - setRange(start: D | null, end: D | null): void; static ɵfac: i0.ɵɵFactoryDef>; static ɵprov: i0.ɵɵInjectableDef>; } @@ -372,16 +379,13 @@ export declare class MatRippleModule { static ɵmod: i0.ɵɵNgModuleDefWithMeta; } -export declare class MatSingleDateSelectionModel extends MatDateSelectionModel { +export declare class MatSingleDateSelectionModel extends MatDateSelectionModel { constructor(adapter: DateAdapter, date?: D | null); add(date: D | null): void; - asDate(): D | null; - compareDate(other: MatSingleDateSelectionModel): number | boolean; isComplete(): boolean; - isSame(other: MatDateSelectionModel): boolean; + isSame(other: MatDateSelectionModel): boolean; isValid(): boolean; overlaps(range: DateRange): boolean; - setDate(date: D | null): void; static ɵfac: i0.ɵɵFactoryDef>; static ɵprov: i0.ɵɵInjectableDef>; }