Skip to content

Commit a82b6d4

Browse files
committed
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.
1 parent b757aee commit a82b6d4

File tree

8 files changed

+185
-145
lines changed

8 files changed

+185
-145
lines changed

src/material/core/datetime/date-selection-model.ts

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,61 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {FactoryProvider, Injectable, OnDestroy, Optional, SkipSelf} from '@angular/core';
10-
import {DateAdapter} from '@angular/material/core';
9+
import {FactoryProvider, Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core';
10+
import {DateAdapter} from './date-adapter';
1111
import {Observable, Subject} from 'rxjs';
1212

1313
/** A class representing a range of dates. */
1414
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;
15+
constructor(
16+
/** The start date of the range. */
17+
readonly start: D | null,
18+
/** The end date of the range. */
19+
readonly end: D | null) {}
20+
}
2121

22-
/** The start date of the range. */
23-
readonly start: D | null;
22+
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
2423

25-
/** The end date of the range. */
26-
readonly end: D | null;
24+
/** Event emitted by the date selection model when its selection changes. */
25+
export interface DateSelectionModelChange<S> {
26+
/** New value for the selection. */
27+
selection: S;
2728

28-
constructor(range?: {start?: D | null, end?: D | null} | null) {
29-
this.start = range && range.start || null;
30-
this.end = range && range.end || null;
31-
}
29+
/** Object that triggered the change. */
30+
source: unknown;
3231
}
3332

34-
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
35-
3633
/** A selection model containing a date selection. */
3734
export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<S>>
3835
implements OnDestroy {
39-
/** Subject used to emit value change events. */
40-
private _valueChangesSubject = new Subject<void>();
36+
private _selectionChanged = new Subject<DateSelectionModelChange<S>>();
4137

42-
/** Observable of value change events. */
43-
valueChanges: Observable<void> = this._valueChangesSubject.asObservable();
38+
/** Emits when the selection has changed. */
39+
selectionChanged: Observable<DateSelectionModelChange<S>> = this._selectionChanged.asObservable();
4440

4541
/** The current selection. */
46-
get selection (): S { return this._selection; }
47-
set selection(s: S) {
48-
this._selection = s;
49-
this._valueChangesSubject.next();
42+
readonly selection: S;
43+
44+
protected constructor(
45+
/** Date adapter used when interacting with dates in the model. */
46+
protected readonly adapter: DateAdapter<D>,
47+
/** Initial selection. */
48+
selection: S) {
49+
this.selection = selection;
5050
}
5151

52-
protected constructor(protected readonly adapter: DateAdapter<D>, private _selection: S) {}
52+
/**
53+
* Updates the current selection in the model.
54+
* @param value New selection that should be assigned.
55+
* @param source Object that triggered the selection change.
56+
*/
57+
updateSelection(value: S, source: unknown) {
58+
(this as {selection: S}).selection = value;
59+
this._selectionChanged.next({selection: value, source});
60+
}
5361

5462
ngOnDestroy() {
55-
this._valueChangesSubject.complete();
63+
this._selectionChanged.complete();
5664
}
5765

5866
/** Adds a date to the current selection. */
@@ -61,10 +69,8 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
6169
/** Checks whether the current selection is complete. */
6270
abstract isComplete(): boolean;
6371

64-
/**
65-
* Checks whether the current selection is the same as the selection in the given selection model.
66-
*/
67-
abstract isSame(other: MatDateSelectionModel<D>): boolean;
72+
/** Checks whether the current selection is identical to the passed-in selection. */
73+
abstract isSame(other: S): boolean;
6874

6975
/** Checks whether the current selection is valid. */
7076
abstract isValid(): boolean;
@@ -76,16 +82,16 @@ export abstract class MatDateSelectionModel<S, D = ExtractDateTypeFromSelection<
7682
/** A selection model that contains a single date. */
7783
@Injectable()
7884
export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | null, D> {
79-
constructor(adapter: DateAdapter<D>, date?: D | null) {
80-
super(adapter, date || null);
85+
constructor(adapter: DateAdapter<D>) {
86+
super(adapter, null);
8187
}
8288

8389
/**
8490
* Adds a date to the current selection. In the case of a single date selection, the added date
8591
* simply overwrites the previous selection
8692
*/
8793
add(date: D | null) {
88-
this.selection = date;
94+
super.updateSelection(date, this);
8995
}
9096

9197
/**
@@ -94,12 +100,9 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | nu
94100
*/
95101
isComplete() { return this.selection != null; }
96102

97-
/**
98-
* Checks whether the current selection is the same as the selection in the given selection model.
99-
*/
100-
isSame(other: MatDateSelectionModel<any>): boolean {
101-
return other instanceof MatSingleDateSelectionModel &&
102-
this.adapter.sameDate(other.selection, this.selection);
103+
/** Checks whether the current selection is identical to the passed-in selection. */
104+
isSame(other: D): boolean {
105+
return this.adapter.sameDate(other, this.selection);
103106
}
104107

105108
/**
@@ -122,8 +125,8 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D | nu
122125
/** A selection model that contains a date range. */
123126
@Injectable()
124127
export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRange<D>, D> {
125-
constructor(adapter: DateAdapter<D>, range?: {start?: D | null, end?: D | null} | null) {
126-
super(adapter, new DateRange(range));
128+
constructor(adapter: DateAdapter<D>) {
129+
super(adapter, new DateRange<D>(null, null));
127130
}
128131

129132
/**
@@ -143,7 +146,7 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
143146
end = null;
144147
}
145148

146-
this.selection = new DateRange<D>({start, end});
149+
super.updateSelection(new DateRange<D>(start, end), this);
147150
}
148151

149152
/**
@@ -154,15 +157,10 @@ export class MatRangeDateSelectionModel<D> extends MatDateSelectionModel<DateRan
154157
return this.selection.start != null && this.selection.end != null;
155158
}
156159

157-
/**
158-
* Checks whether the current selection is the same as the selection in the given selection model.
159-
*/
160-
isSame(other: MatDateSelectionModel<any>): boolean {
161-
if (other instanceof MatRangeDateSelectionModel) {
162-
return this.adapter.sameDate(this.selection.start, other.selection.start) &&
163-
this.adapter.sameDate(this.selection.end, other.selection.end);
164-
}
165-
return false;
160+
/** Checks whether the current selection is identical to the passed-in selection. */
161+
isSame(other: DateRange<D>): boolean {
162+
return this.adapter.sameDate(this.selection.start, other.start) &&
163+
this.adapter.sameDate(this.selection.end, other.end);
166164
}
167165

168166
/**

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
}
@@ -361,9 +381,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
361381

362382
/** Handles date selection in the month view. */
363383
_dateSelected(date: D | null): void {
364-
if (date && !this._dateAdapter.sameDate(date, this.selected)) {
365-
this.selectedChange.emit(date);
366-
}
384+
this._model.add(date);
367385
}
368386

369387
/** 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)