Skip to content

Commit 5e8a974

Browse files
authored
refactor(datepicker): move date range selection logic out of calendar (#19219)
There are some projects that expect that the `mat-calendar` doesn't assign the value itself, but only renders it out and emits an event when something is selected. These changes move the date selection logic out into the `MatDatepickerContent`. Also renames the calendar range selection strategy since it doesn't have much to do with the calendar anymore.
1 parent 125475f commit 5e8a974

File tree

11 files changed

+100
-87
lines changed

11 files changed

+100
-87
lines changed

src/dev-app/datepicker/datepicker-demo.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import {
2424
MatCalendar,
2525
MatCalendarHeader,
2626
MatDatepickerInputEvent,
27-
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
28-
MatCalendarRangeSelectionStrategy,
27+
MAT_DATE_RANGE_SELECTION_STRATEGY,
28+
MatDateRangeSelectionStrategy,
2929
DateRange
3030
} from '@angular/material/datepicker';
3131
import {Subject} from 'rxjs';
@@ -86,7 +86,7 @@ export class DatepickerDemo {
8686

8787
/** Range selection strategy that preserves the current range. */
8888
@Injectable()
89-
export class PreserveRangeStrategy<D> implements MatCalendarRangeSelectionStrategy<D> {
89+
export class PreserveRangeStrategy<D> implements MatDateRangeSelectionStrategy<D> {
9090
constructor(private _dateAdapter: DateAdapter<D>) {}
9191

9292
selectionFinished(date: D, currentRange: DateRange<D>) {
@@ -134,7 +134,7 @@ export class PreserveRangeStrategy<D> implements MatCalendarRangeSelectionStrate
134134
@Directive({
135135
selector: '[customRangeStrategy]',
136136
providers: [{
137-
provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
137+
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
138138
useClass: PreserveRangeStrategy
139139
}]
140140
})

src/material/datepicker/calendar.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<mat-month-view
55
*ngSwitchCase="'month'"
66
[(activeDate)]="activeDate"
7-
[selected]="_getDisplaySelection()"
7+
[selected]="selected"
88
[dateFilter]="dateFilter"
99
[maxDate]="maxDate"
1010
[minDate]="minDate"
@@ -17,7 +17,7 @@
1717
<mat-year-view
1818
*ngSwitchCase="'year'"
1919
[(activeDate)]="activeDate"
20-
[selected]="_getDisplaySelection()"
20+
[selected]="selected"
2121
[dateFilter]="dateFilter"
2222
[maxDate]="maxDate"
2323
[minDate]="minDate"
@@ -28,7 +28,7 @@
2828
<mat-multi-year-view
2929
*ngSwitchCase="'multi-year'"
3030
[(activeDate)]="activeDate"
31-
[selected]="_getDisplaySelection()"
31+
[selected]="selected"
3232
[dateFilter]="dateFilter"
3333
[maxDate]="maxDate"
3434
[minDate]="minDate"

src/material/datepicker/calendar.ts

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,7 @@ import {
4242
yearsPerPage
4343
} from './multi-year-view';
4444
import {MatYearView} from './year-view';
45-
import {
46-
MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER,
47-
DateRange,
48-
MatDateSelectionModel,
49-
} from './date-selection-model';
50-
import {
51-
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
52-
MatCalendarRangeSelectionStrategy
53-
} from './calendar-range-selection-strategy';
45+
import {MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER, DateRange} from './date-selection-model';
5446

5547
/**
5648
* Possible views for the calendar.
@@ -223,15 +215,15 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
223215

224216
/** The currently selected date. */
225217
@Input()
226-
get selected(): DateRange<D> | D | null { return this._model.selection; }
218+
get selected(): DateRange<D> | D | null { return this._selected; }
227219
set selected(value: DateRange<D> | D | null) {
228220
if (value instanceof DateRange) {
229-
this._model.updateSelection(value, this);
221+
this._selected = value;
230222
} else {
231-
const newValue = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
232-
this._model.updateSelection(newValue!, this);
223+
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
233224
}
234225
}
226+
private _selected: DateRange<D> | D | null;
235227

236228
/** The minimum selectable date. */
237229
@Input()
@@ -280,7 +272,8 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
280272
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
281273

282274
/** Emits when any date is selected. */
283-
@Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();
275+
@Output() readonly _userSelection: EventEmitter<MatCalendarUserEvent<D | null>> =
276+
new EventEmitter<MatCalendarUserEvent<D | null>>();
284277

285278
/** Reference to the current month view component. */
286279
@ViewChild(MatMonthView) monthView: MatMonthView<D>;
@@ -320,10 +313,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
320313
constructor(_intl: MatDatepickerIntl,
321314
@Optional() private _dateAdapter: DateAdapter<D>,
322315
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
323-
private _changeDetectorRef: ChangeDetectorRef,
324-
private _model: MatDateSelectionModel<DateRange<D> | D | null>,
325-
@Optional() @Inject(MAT_CALENDAR_RANGE_SELECTION_STRATEGY)
326-
private _rangeSelectionStrategy?: MatCalendarRangeSelectionStrategy<D>) {
316+
private _changeDetectorRef: ChangeDetectorRef) {
327317

328318
if (!this._dateAdapter) {
329319
throw createMissingDateImplError('DateAdapter');
@@ -399,26 +389,16 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
399389

400390
/** Handles date selection in the month view. */
401391
_dateSelected(event: MatCalendarUserEvent<D | null>): void {
402-
const selection = this._model.selection;
403-
const value = event.value;
404-
const isRange = selection instanceof DateRange;
405-
406-
// If we're selecting a range and we have a selection strategy, always pass the value through
407-
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
408-
// A null value when picking a range means that the user cancelled the selection (e.g. by
409-
// pressing escape), whereas when selecting a single value it means that the value didn't
410-
// change. This isn't very intuitive, but it's here for backwards-compatibility.
411-
if (isRange && this._rangeSelectionStrategy) {
412-
const newSelection = this._rangeSelectionStrategy.selectionFinished(value,
413-
selection as DateRange<D>, event.event);
414-
this._model.updateSelection(newSelection, this);
415-
this.selectedChange.emit(value!);
416-
} else if (value && (isRange || !this._dateAdapter.sameDate(value, this.selected as D))) {
417-
this._model.add(value);
418-
this.selectedChange.emit(value);
392+
const date = event.value;
393+
394+
if (this.selected instanceof DateRange ||
395+
(date && !this._dateAdapter.sameDate(date, this.selected))) {
396+
// @breaking-change 11.0.0 remove non-null assertion
397+
// once the `selectedChange` is allowed to be null.
398+
this.selectedChange.emit(date!);
419399
}
420400

421-
this._userSelection.emit();
401+
this._userSelection.emit(event);
422402
}
423403

424404
/** Handles year selection in the multiyear view. */
@@ -437,11 +417,6 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
437417
this.currentView = view;
438418
}
439419

440-
/** Gets the selection that should be displayed to the user. */
441-
_getDisplaySelection(): DateRange<D> | D | null {
442-
return this._model.isValid() ? this._model.selection : null;
443-
}
444-
445420
/**
446421
* @param obj The object to check.
447422
* @returns The given object if it is both a date instance and valid, otherwise null.

src/material/datepicker/calendar-range-selection-strategy.ts renamed to src/material/datepicker/date-range-selection-strategy.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@ import {DateRange} from './date-selection-model';
1313
// TODO(crisbeto): this needs to be expanded to allow for the preview range to be customized.
1414

1515
/** Injection token used to customize the date range selection behavior. */
16-
export const MAT_CALENDAR_RANGE_SELECTION_STRATEGY =
17-
new InjectionToken<MatCalendarRangeSelectionStrategy<any>>(
18-
'MAT_CALENDAR_RANGE_SELECTION_STRATEGY');
16+
export const MAT_DATE_RANGE_SELECTION_STRATEGY =
17+
new InjectionToken<MatDateRangeSelectionStrategy<any>>('MAT_DATE_RANGE_SELECTION_STRATEGY');
1918

2019
/** Object that can be provided in order to customize the date range selection behavior. */
21-
export interface MatCalendarRangeSelectionStrategy<D> {
20+
export interface MatDateRangeSelectionStrategy<D> {
2221
/**
2322
* Called when the user has finished selecting a value.
2423
* @param date Date that was selected. Will be null if the user cleared the selection.
@@ -43,7 +42,7 @@ export interface MatCalendarRangeSelectionStrategy<D> {
4342

4443
/** Provides the default date range selection behavior. */
4544
@Injectable()
46-
export class DefaultMatCalendarRangeStrategy<D> implements MatCalendarRangeSelectionStrategy<D> {
45+
export class DefaultMatCalendarRangeStrategy<D> implements MatDateRangeSelectionStrategy<D> {
4746
constructor(private _dateAdapter: DateAdapter<D>) {}
4847

4948
selectionFinished(date: D, currentRange: DateRange<D>) {

src/material/datepicker/datepicker-base.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,17 @@ import {filter, take} from 'rxjs/operators';
5353
import {MatCalendar} from './calendar';
5454
import {matDatepickerAnimations} from './datepicker-animations';
5555
import {createMissingDateImplError} from './datepicker-errors';
56-
import {MatCalendarCellCssClasses} from './calendar-body';
56+
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
5757
import {DateFilterFn} from './datepicker-input-base';
58-
import {ExtractDateTypeFromSelection, MatDateSelectionModel} from './date-selection-model';
58+
import {
59+
ExtractDateTypeFromSelection,
60+
MatDateSelectionModel,
61+
DateRange,
62+
} from './date-selection-model';
63+
import {
64+
MAT_DATE_RANGE_SELECTION_STRATEGY,
65+
MatDateRangeSelectionStrategy,
66+
} from './date-range-selection-strategy';
5967

6068
/** Used to generate a unique ID for each datepicker instance. */
6169
let datepickerUid = 0;
@@ -143,11 +151,15 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
143151
constructor(
144152
elementRef: ElementRef,
145153
/**
146-
* @deprecated `_changeDetectorRef` and `_model` parameters to become required.
154+
* @deprecated `_changeDetectorRef`, `_model` and `_rangeSelectionStrategy`
155+
* parameters to become required.
147156
* @breaking-change 11.0.0
148157
*/
149158
private _changeDetectorRef?: ChangeDetectorRef,
150-
private _model?: MatDateSelectionModel<S, D>) {
159+
private _model?: MatDateSelectionModel<S, D>,
160+
private _dateAdapter?: DateAdapter<D>,
161+
@Optional() @Inject(MAT_DATE_RANGE_SELECTION_STRATEGY)
162+
private _rangeSelectionStrategy?: MatDateRangeSelectionStrategy<D>) {
151163
super(elementRef);
152164
}
153165

@@ -159,8 +171,29 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
159171
this._animationDone.complete();
160172
}
161173

162-
_handleUserSelection() {
163-
// @breaking-change 11.0.0 Remove null check for _model.
174+
_handleUserSelection(event: MatCalendarUserEvent<D | null>) {
175+
// @breaking-change 11.0.0 Remove null checks for _model,
176+
// _rangeSelectionStrategy and _dateAdapter.
177+
if (this._model && this._dateAdapter) {
178+
const selection = this._model.selection;
179+
const value = event.value;
180+
const isRange = selection instanceof DateRange;
181+
182+
// If we're selecting a range and we have a selection strategy, always pass the value through
183+
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
184+
// A null value when picking a range means that the user cancelled the selection (e.g. by
185+
// pressing escape), whereas when selecting a single value it means that the value didn't
186+
// change. This isn't very intuitive, but it's here for backwards-compatibility.
187+
if (isRange && this._rangeSelectionStrategy) {
188+
const newSelection = this._rangeSelectionStrategy.selectionFinished(value,
189+
selection as unknown as DateRange<D>, event.event);
190+
this._model.updateSelection(newSelection as unknown as S, this);
191+
} else if (value && (isRange ||
192+
!this._dateAdapter.sameDate(value, selection as unknown as D))) {
193+
this._model.add(value);
194+
}
195+
}
196+
164197
if (!this._model || this._model.isComplete()) {
165198
this.datepicker.close();
166199
}
@@ -174,6 +207,11 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
174207
this._changeDetectorRef.markForCheck();
175208
}
176209
}
210+
211+
_getSelected() {
212+
// @breaking-change 11.0.0 Remove null check for `_model`.
213+
return this._model ? this._model.selection as unknown as D | DateRange<D> | null : null;
214+
}
177215
}
178216

179217
/** Form control that can be associated with a datepicker. */

src/material/datepicker/datepicker-content.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
[maxDate]="datepicker._maxDate"
88
[dateFilter]="datepicker._dateFilter"
99
[headerComponent]="datepicker.calendarHeaderComponent"
10+
[selected]="_getSelected()"
1011
[dateClass]="datepicker.dateClass"
1112
[comparisonStart]="comparisonStart"
1213
[comparisonEnd]="comparisonEnd"
1314
[@fadeInCalendar]="'enter'"
1415
(yearSelected)="datepicker._selectYear($event)"
1516
(monthSelected)="datepicker._selectMonth($event)"
16-
(_userSelection)="_handleUserSelection()">
17+
(_userSelection)="_handleUserSelection($event)">
1718
</mat-calendar>

src/material/datepicker/datepicker-module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import {MatDateRangeInput} from './date-range-input';
3131
import {MatStartDate, MatEndDate} from './date-range-input-parts';
3232
import {MatDateRangePicker} from './date-range-picker';
3333
import {
34-
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
34+
MAT_DATE_RANGE_SELECTION_STRATEGY,
3535
DefaultMatCalendarRangeStrategy
36-
} from './calendar-range-selection-strategy';
36+
} from './date-range-selection-strategy';
3737

3838

3939
@NgModule({
@@ -84,7 +84,7 @@ import {
8484
MatDatepickerIntl,
8585
MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER,
8686
{
87-
provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
87+
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
8888
useClass: DefaultMatCalendarRangeStrategy
8989
}
9090
],

src/material/datepicker/month-view.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import {MatCalendarBody} from './calendar-body';
2828
import {MatMonthView} from './month-view';
2929
import {DateRange} from './date-selection-model';
3030
import {
31-
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
31+
MAT_DATE_RANGE_SELECTION_STRATEGY,
3232
DefaultMatCalendarRangeStrategy,
33-
} from './calendar-range-selection-strategy';
33+
} from './date-range-selection-strategy';
3434

3535
describe('MatMonthView', () => {
3636
let dir: {value: Direction};
@@ -51,7 +51,7 @@ describe('MatMonthView', () => {
5151
],
5252
providers: [
5353
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}},
54-
{provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY, useClass: DefaultMatCalendarRangeStrategy}
54+
{provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: DefaultMatCalendarRangeStrategy}
5555
]
5656
});
5757

src/material/datepicker/month-view.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ import {Subscription} from 'rxjs';
4646
import {startWith} from 'rxjs/operators';
4747
import {DateRange} from './date-selection-model';
4848
import {
49-
MatCalendarRangeSelectionStrategy,
50-
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
51-
} from './calendar-range-selection-strategy';
49+
MatDateRangeSelectionStrategy,
50+
MAT_DATE_RANGE_SELECTION_STRATEGY,
51+
} from './date-range-selection-strategy';
5252

5353

5454
const DAYS_PER_WEEK = 7;
@@ -179,8 +179,8 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
179179
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
180180
@Optional() public _dateAdapter: DateAdapter<D>,
181181
@Optional() private _dir?: Directionality,
182-
@Inject(MAT_CALENDAR_RANGE_SELECTION_STRATEGY) @Optional()
183-
private _rangeStrategy?: MatCalendarRangeSelectionStrategy<D>) {
182+
@Inject(MAT_DATE_RANGE_SELECTION_STRATEGY) @Optional()
183+
private _rangeStrategy?: MatDateRangeSelectionStrategy<D>) {
184184
if (!this._dateAdapter) {
185185
throw createMissingDateImplError('DateAdapter');
186186
}

src/material/datepicker/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export * from './datepicker-module';
1010
export * from './calendar';
1111
export * from './calendar-body';
1212
export * from './datepicker';
13-
export * from './calendar-range-selection-strategy';
13+
export * from './date-range-selection-strategy';
1414
export * from './datepicker-animations';
1515
export {
1616
MAT_DATEPICKER_SCROLL_STRATEGY,

0 commit comments

Comments
 (0)