Skip to content

Commit b9dc8ea

Browse files
crisbetoxlou978jaguimammalerbaroboshoes
committed
refactor(datepicker): implement date selection model provider (#18090)
* Adds a date selection model (#17363) * Adds a date selection model * Adds a date selection model * Use MatDateSelectionModel to model the selected value in MatDatepickerInput (#17497) * Make the selection model injectable and add an `overlaps` method (#17766) * Adds a date selection model (#17363) * Adds a date selection model * Adds a date selection model * Add overlaps function to date selection model * Trying to incorporate the new type definition (#17942) * incoporate new type definition * fix test and clean up the types a bit * update api gold * fix some nits * fix lint Co-authored-by: mmalerba <mmalerba@google.com> * refactor(datepicker): implement date selection model provider Reworks the datepicker-related directives to move the source of the value into the new datepicker selection provider. Co-authored-by: xlou978 <51772166+xlou978@users.noreply.github.com> Co-authored-by: jaguima <jaguima@gmail.com> Co-authored-by: mmalerba <mmalerba@google.com> Co-authored-by: roboshoes <mail@mathias-paumgarten.com> Co-authored-by: goldblatt <suzgoldblatt@gmail.com>
1 parent 68a2f89 commit b9dc8ea

File tree

9 files changed

+400
-80
lines changed

9 files changed

+400
-80
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {FactoryProvider, Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
10+
import {DateAdapter} from './date-adapter';
11+
import {Observable, Subject} from 'rxjs';
12+
13+
/** A class representing a range of dates. */
14+
export class DateRange<D> {
15+
/**
16+
* Ensures that objects with a `start` and `end` property can't be assigned to a variable that
17+
* expects a `DateRange`
18+
*/
19+
// tslint:disable-next-line:no-unused-variable
20+
private _disableStructuralEquivalency: never;
21+
22+
constructor(
23+
/** The start date of the range. */
24+
readonly start: D | null,
25+
/** The end date of the range. */
26+
readonly end: D | null) {}
27+
}
28+
29+
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
30+
31+
/** Event emitted by the date selection model when its selection changes. */
32+
export interface DateSelectionModelChange<S> {
33+
/** New value for the selection. */
34+
selection: S;
35+
36+
/** Object that triggered the change. */
37+
source: unknown;
38+
}
39+
40+
/** A selection model containing a date selection. */
41+
export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>>
42+
implements OnDestroy {
43+
private _selectionChanged = new Subject<DateSelectionModelChange<S>>();
44+
45+
/** Emits when the selection has changed. */
46+
selectionChanged: Observable<DateSelectionModelChange<S>> = this._selectionChanged.asObservable();
47+
48+
protected constructor(
49+
/** Date adapter used when interacting with dates in the model. */
50+
protected readonly adapter: DateAdapter<D>,
51+
/** The current selection. */
52+
readonly selection: S) {
53+
this.selection = selection;
54+
}
55+
56+
/**
57+
* Updates the current selection in the model.
58+
* @param value New selection that should be assigned.
59+
* @param source Object that triggered the selection change.
60+
*/
61+
updateSelection(value: S, source: unknown) {
62+
(this as {selection: S}).selection = value;
63+
this._selectionChanged.next({selection: value, source});
64+
}
65+
66+
ngOnDestroy() {
67+
this._selectionChanged.complete();
68+
}
69+
70+
/** Adds a date to the current selection. */
71+
abstract add(date: D | null): void;
72+
73+
/** Checks whether the current selection is complete. */
74+
abstract isComplete(): boolean;
75+
76+
/** Checks whether the current selection is identical to the passed-in selection. */
77+
abstract isSame(other: S): boolean;
78+
79+
/** Checks whether the current selection is valid. */
80+
abstract isValid(): boolean;
81+
82+
/** Checks whether the current selection overlaps with the given range. */
83+
abstract overlaps(range: DateRange<D>): boolean;
84+
}
85+
86+
/** A selection model that contains a single date. */
87+
@Injectable()
88+
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> {
89+
constructor(adapter: DateAdapter<D>) {
90+
super(adapter, null);
91+
}
92+
93+
/**
94+
* Adds a date to the current selection. In the case of a single date selection, the added date
95+
* simply overwrites the previous selection
96+
*/
97+
add(date: D | null) {
98+
super.updateSelection(date, this);
99+
}
100+
101+
/**
102+
* Checks whether the current selection is complete. In the case of a single date selection, this
103+
* is true if the current selection is not null.
104+
*/
105+
isComplete() { return this.selection != null; }
106+
107+
/** Checks whether the current selection is identical to the passed-in selection. */
108+
isSame(other: D): boolean {
109+
return this.adapter.sameDate(other, this.selection);
110+
}
111+
112+
/**
113+
* Checks whether the current selection is valid. In the case of a single date selection, this
114+
* means that the current selection is not null and is a valid date.
115+
*/
116+
isValid(): boolean {
117+
return this.selection != null && this.adapter.isDateInstance(this.selection) &&
118+
this.adapter.isValid(this.selection);
119+
}
120+
121+
/** Checks whether the current selection overlaps with the given range. */
122+
overlaps(range: DateRange<D>): boolean {
123+
return !!(this.selection && range.start && range.end &&
124+
this.adapter.compareDate(range.start, this.selection) <= 0 &&
125+
this.adapter.compareDate(this.selection, range.end) <= 0);
126+
}
127+
}
128+
129+
/** A selection model that contains a date range. */
130+
@Injectable()
131+
export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> {
132+
constructor(adapter: DateAdapter<D>) {
133+
super(adapter, new DateRange<D>(null, null));
134+
}
135+
136+
/**
137+
* Adds a date to the current selection. In the case of a date range selection, the added date
138+
* fills in the next `null` value in the range. If both the start and the end already have a date,
139+
* the selection is reset so that the given date is the new `start` and the `end` is null.
140+
*/
141+
add(date: D | null): void {
142+
let {start, end} = this.selection;
143+
144+
if (start == null) {
145+
start = date;
146+
} else if (end == null) {
147+
end = date;
148+
} else {
149+
start = date;
150+
end = null;
151+
}
152+
153+
super.updateSelection(new DateRange<D>(start, end), this);
154+
}
155+
156+
/**
157+
* Checks whether the current selection is complete. In the case of a date range selection, this
158+
* is true if the current selection has a non-null `start` and `end`.
159+
*/
160+
isComplete(): boolean {
161+
return this.selection.start != null && this.selection.end != null;
162+
}
163+
164+
/** Checks whether the current selection is identical to the passed-in selection. */
165+
isSame(other: DateRange<D>): boolean {
166+
return this.adapter.sameDate(this.selection.start, other.start) &&
167+
this.adapter.sameDate(this.selection.end, other.end);
168+
}
169+
170+
/**
171+
* Checks whether the current selection is valid. In the case of a date range selection, this
172+
* means that the current selection has a `start` and `end` that are both non-null and valid
173+
* dates.
174+
*/
175+
isValid(): boolean {
176+
return this.selection.start != null && this.selection.end != null &&
177+
this.adapter.isValid(this.selection.start!) && this.adapter.isValid(this.selection.end!);
178+
}
179+
180+
/**
181+
* Returns true if the given range and the selection overlap in any way. False if otherwise, that
182+
* includes incomplete selections or ranges.
183+
*/
184+
overlaps(range: DateRange<D>): boolean {
185+
if (!(this.selection.start && this.selection.end && range.start && range.end)) {
186+
return false;
187+
}
188+
189+
return (
190+
this._isBetween(range.start, this.selection.start, this.selection.end) ||
191+
this._isBetween(range.end, this.selection.start, this.selection.end) ||
192+
(
193+
this.adapter.compareDate(range.start, this.selection.start) <= 0 &&
194+
this.adapter.compareDate(this.selection.end, range.end) <= 0
195+
)
196+
);
197+
}
198+
199+
private _isBetween(value: D, from: D, to: D): boolean {
200+
return this.adapter.compareDate(from, value) <= 0 && this.adapter.compareDate(value, to) <= 0;
201+
}
202+
}
203+
204+
/** @docs-private */
205+
export function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(
206+
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
207+
return parent || new MatSingleDateSelectionModel(adapter);
208+
}
209+
210+
/** Used to provide a single selection model to a component. */
211+
export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
212+
provide: MatDateSelectionModel,
213+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
214+
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
215+
};

src/material/core/datetime/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './date-adapter';
1717
export * from './date-formats';
1818
export * from './native-date-adapter';
1919
export * from './native-date-formats';
20+
export * from './date-selection-model';
2021

2122

2223
@NgModule({

src/material/datepicker/calendar.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ import {
2525
ViewChild,
2626
ViewEncapsulation,
2727
} from '@angular/core';
28-
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
28+
import {
29+
DateAdapter,
30+
MAT_DATE_FORMATS,
31+
MatDateFormats,
32+
MatDateSelectionModel,
33+
MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER,
34+
} from '@angular/material/core';
2935
import {Subject, Subscription} from 'rxjs';
3036
import {MatCalendarCellCssClasses} from './calendar-body';
3137
import {createMissingDateImplError} from './datepicker-errors';
@@ -179,6 +185,7 @@ export class MatCalendarHeader<D> {
179185
exportAs: 'matCalendar',
180186
encapsulation: ViewEncapsulation.None,
181187
changeDetection: ChangeDetectionStrategy.OnPush,
188+
providers: [MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER]
182189
})
183190
export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges {
184191
/** An input indicating the type of the header component, if set. */
@@ -188,6 +195,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
188195
_calendarHeaderPortal: Portal<any>;
189196

190197
private _intlChanges: Subscription;
198+
private _selectedChanges: Subscription;
191199

192200
/**
193201
* Used for scheduling that focus should be moved to the active cell on the next tick.
@@ -209,11 +217,11 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
209217

210218
/** The currently selected date. */
211219
@Input()
212-
get selected(): D | null { return this._selected; }
220+
get selected(): D | null { return this._model.selection; }
213221
set selected(value: D | null) {
214-
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
222+
const newValue = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
223+
this._model.updateSelection(newValue, this);
215224
}
216-
private _selected: D | null;
217225

218226
/** The minimum selectable date. */
219227
@Input()
@@ -237,7 +245,10 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
237245
/** Function that can be used to add custom CSS classes to dates. */
238246
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
239247

240-
/** Emits when the currently selected date changes. */
248+
/**
249+
* Emits when the currently selected date changes.
250+
* @breaking-change 11.0.0 Emitted value to change to `D | null`.
251+
*/
241252
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
242253

243254
/**
@@ -293,7 +304,8 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
293304
constructor(_intl: MatDatepickerIntl,
294305
@Optional() private _dateAdapter: DateAdapter<D>,
295306
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
296-
private _changeDetectorRef: ChangeDetectorRef) {
307+
private _changeDetectorRef: ChangeDetectorRef,
308+
private _model: MatDateSelectionModel<D | null, D>) {
297309

298310
if (!this._dateAdapter) {
299311
throw createMissingDateImplError('DateAdapter');
@@ -307,6 +319,13 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
307319
_changeDetectorRef.markForCheck();
308320
this.stateChanges.next();
309321
});
322+
323+
this._selectedChanges = _model.selectionChanged.subscribe(event => {
324+
// @breaking-change 11.0.0 Remove null check once `event.selection` is allowed to be null.
325+
if (event.selection) {
326+
this.selectedChange.emit(event.selection);
327+
}
328+
});
310329
}
311330

312331
ngAfterContentInit() {
@@ -325,6 +344,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
325344
}
326345

327346
ngOnDestroy() {
347+
this._selectedChanges.unsubscribe();
328348
this._intlChanges.unsubscribe();
329349
this.stateChanges.complete();
330350
}
@@ -369,9 +389,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
369389

370390
/** Handles date selection in the month view. */
371391
_dateSelected(date: D | null): void {
372-
if (date && !this._dateAdapter.sameDate(date, this.selected)) {
373-
this.selectedChange.emit(date);
374-
}
392+
this._model.add(date);
375393
}
376394

377395
/** Handles year selection in the multiyear view. */

src/material/datepicker/datepicker-content.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
[maxDate]="datepicker._maxDate"
88
[dateFilter]="datepicker._dateFilter"
99
[headerComponent]="datepicker.calendarHeaderComponent"
10-
[selected]="datepicker._selected"
1110
[dateClass]="datepicker.dateClass"
1211
[@fadeInCalendar]="'enter'"
13-
(selectedChange)="datepicker.select($event)"
1412
(yearSelected)="datepicker._selectYear($event)"
1513
(monthSelected)="datepicker._selectMonth($event)"
1614
(_userSelection)="datepicker.close()">

0 commit comments

Comments
 (0)