Skip to content

refactor(datepicker): move date range selection logic out of calendar #19219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/dev-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
MatCalendar,
MatCalendarHeader,
MatDatepickerInputEvent,
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
MatCalendarRangeSelectionStrategy,
MAT_DATE_RANGE_SELECTION_STRATEGY,
MatDateRangeSelectionStrategy,
DateRange
} from '@angular/material/datepicker';
import {Subject} from 'rxjs';
Expand Down Expand Up @@ -86,7 +86,7 @@ export class DatepickerDemo {

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

selectionFinished(date: D, currentRange: DateRange<D>) {
Expand Down Expand Up @@ -134,7 +134,7 @@ export class PreserveRangeStrategy<D> implements MatCalendarRangeSelectionStrate
@Directive({
selector: '[customRangeStrategy]',
providers: [{
provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
useClass: PreserveRangeStrategy
}]
})
Expand Down
6 changes: 3 additions & 3 deletions src/material/datepicker/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<mat-month-view
*ngSwitchCase="'month'"
[(activeDate)]="activeDate"
[selected]="_getDisplaySelection()"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
Expand All @@ -17,7 +17,7 @@
<mat-year-view
*ngSwitchCase="'year'"
[(activeDate)]="activeDate"
[selected]="_getDisplaySelection()"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
Expand All @@ -28,7 +28,7 @@
<mat-multi-year-view
*ngSwitchCase="'multi-year'"
[(activeDate)]="activeDate"
[selected]="_getDisplaySelection()"
[selected]="selected"
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
Expand Down
57 changes: 16 additions & 41 deletions src/material/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,7 @@ import {
yearsPerPage
} from './multi-year-view';
import {MatYearView} from './year-view';
import {
MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER,
DateRange,
MatDateSelectionModel,
} from './date-selection-model';
import {
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
MatCalendarRangeSelectionStrategy
} from './calendar-range-selection-strategy';
import {MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER, DateRange} from './date-selection-model';

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

/** The currently selected date. */
@Input()
get selected(): DateRange<D> | D | null { return this._model.selection; }
get selected(): DateRange<D> | D | null { return this._selected; }
set selected(value: DateRange<D> | D | null) {
if (value instanceof DateRange) {
this._model.updateSelection(value, this);
this._selected = value;
} else {
const newValue = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._model.updateSelection(newValue!, this);
this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
}
}
private _selected: DateRange<D> | D | null;

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

/** Emits when any date is selected. */
@Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();
@Output() readonly _userSelection: EventEmitter<MatCalendarUserEvent<D | null>> =
new EventEmitter<MatCalendarUserEvent<D | null>>();

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

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

/** Handles date selection in the month view. */
_dateSelected(event: MatCalendarUserEvent<D | null>): void {
const selection = this._model.selection;
const value = event.value;
const isRange = selection instanceof DateRange;

// If we're selecting a range and we have a selection strategy, always pass the value through
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
// A null value when picking a range means that the user cancelled the selection (e.g. by
// pressing escape), whereas when selecting a single value it means that the value didn't
// change. This isn't very intuitive, but it's here for backwards-compatibility.
if (isRange && this._rangeSelectionStrategy) {
const newSelection = this._rangeSelectionStrategy.selectionFinished(value,
selection as DateRange<D>, event.event);
this._model.updateSelection(newSelection, this);
this.selectedChange.emit(value!);
} else if (value && (isRange || !this._dateAdapter.sameDate(value, this.selected as D))) {
this._model.add(value);
this.selectedChange.emit(value);
const date = event.value;

if (this.selected instanceof DateRange ||
(date && !this._dateAdapter.sameDate(date, this.selected))) {
// @breaking-change 11.0.0 remove non-null assertion
// once the `selectedChange` is allowed to be null.
this.selectedChange.emit(date!);
}

this._userSelection.emit();
this._userSelection.emit(event);
}

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

/** Gets the selection that should be displayed to the user. */
_getDisplaySelection(): DateRange<D> | D | null {
return this._model.isValid() ? this._model.selection : null;
}

/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import {DateRange} from './date-selection-model';
// TODO(crisbeto): this needs to be expanded to allow for the preview range to be customized.

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

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

/** Provides the default date range selection behavior. */
@Injectable()
export class DefaultMatCalendarRangeStrategy<D> implements MatCalendarRangeSelectionStrategy<D> {
export class DefaultMatCalendarRangeStrategy<D> implements MatDateRangeSelectionStrategy<D> {
constructor(private _dateAdapter: DateAdapter<D>) {}

selectionFinished(date: D, currentRange: DateRange<D>) {
Expand Down
50 changes: 44 additions & 6 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ import {filter, take} from 'rxjs/operators';
import {MatCalendar} from './calendar';
import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {MatCalendarCellCssClasses} from './calendar-body';
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
import {DateFilterFn} from './datepicker-input-base';
import {ExtractDateTypeFromSelection, MatDateSelectionModel} from './date-selection-model';
import {
ExtractDateTypeFromSelection,
MatDateSelectionModel,
DateRange,
} from './date-selection-model';
import {
MAT_DATE_RANGE_SELECTION_STRATEGY,
MatDateRangeSelectionStrategy,
} from './date-range-selection-strategy';

/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
Expand Down Expand Up @@ -143,11 +151,15 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
constructor(
elementRef: ElementRef,
/**
* @deprecated `_changeDetectorRef` and `_model` parameters to become required.
* @deprecated `_changeDetectorRef`, `_model` and `_rangeSelectionStrategy`
* parameters to become required.
* @breaking-change 11.0.0
*/
private _changeDetectorRef?: ChangeDetectorRef,
private _model?: MatDateSelectionModel<S, D>) {
private _model?: MatDateSelectionModel<S, D>,
private _dateAdapter?: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_RANGE_SELECTION_STRATEGY)
private _rangeSelectionStrategy?: MatDateRangeSelectionStrategy<D>) {
super(elementRef);
}

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

_handleUserSelection() {
// @breaking-change 11.0.0 Remove null check for _model.
_handleUserSelection(event: MatCalendarUserEvent<D | null>) {
// @breaking-change 11.0.0 Remove null checks for _model,
// _rangeSelectionStrategy and _dateAdapter.
if (this._model && this._dateAdapter) {
const selection = this._model.selection;
const value = event.value;
const isRange = selection instanceof DateRange;

// If we're selecting a range and we have a selection strategy, always pass the value through
// there. Otherwise don't assign null values to the model, unless we're selecting a range.
// A null value when picking a range means that the user cancelled the selection (e.g. by
// pressing escape), whereas when selecting a single value it means that the value didn't
// change. This isn't very intuitive, but it's here for backwards-compatibility.
if (isRange && this._rangeSelectionStrategy) {
const newSelection = this._rangeSelectionStrategy.selectionFinished(value,
selection as unknown as DateRange<D>, event.event);
this._model.updateSelection(newSelection as unknown as S, this);
} else if (value && (isRange ||
!this._dateAdapter.sameDate(value, selection as unknown as D))) {
this._model.add(value);
}
}

if (!this._model || this._model.isComplete()) {
this.datepicker.close();
}
Expand All @@ -174,6 +207,11 @@ export class MatDatepickerContent<S, D = ExtractDateTypeFromSelection<S>>
this._changeDetectorRef.markForCheck();
}
}

_getSelected() {
// @breaking-change 11.0.0 Remove null check for `_model`.
return this._model ? this._model.selection as unknown as D | DateRange<D> | null : null;
}
}

/** Form control that can be associated with a datepicker. */
Expand Down
3 changes: 2 additions & 1 deletion src/material/datepicker/datepicker-content.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[headerComponent]="datepicker.calendarHeaderComponent"
[selected]="_getSelected()"
[dateClass]="datepicker.dateClass"
[comparisonStart]="comparisonStart"
[comparisonEnd]="comparisonEnd"
[@fadeInCalendar]="'enter'"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
(_userSelection)="_handleUserSelection()">
(_userSelection)="_handleUserSelection($event)">
</mat-calendar>
6 changes: 3 additions & 3 deletions src/material/datepicker/datepicker-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import {MatDateRangeInput} from './date-range-input';
import {MatStartDate, MatEndDate} from './date-range-input-parts';
import {MatDateRangePicker} from './date-range-picker';
import {
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
MAT_DATE_RANGE_SELECTION_STRATEGY,
DefaultMatCalendarRangeStrategy
} from './calendar-range-selection-strategy';
} from './date-range-selection-strategy';


@NgModule({
Expand Down Expand Up @@ -84,7 +84,7 @@ import {
MatDatepickerIntl,
MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER,
{
provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
useClass: DefaultMatCalendarRangeStrategy
}
],
Expand Down
6 changes: 3 additions & 3 deletions src/material/datepicker/month-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import {MatCalendarBody} from './calendar-body';
import {MatMonthView} from './month-view';
import {DateRange} from './date-selection-model';
import {
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
MAT_DATE_RANGE_SELECTION_STRATEGY,
DefaultMatCalendarRangeStrategy,
} from './calendar-range-selection-strategy';
} from './date-range-selection-strategy';

describe('MatMonthView', () => {
let dir: {value: Direction};
Expand All @@ -51,7 +51,7 @@ describe('MatMonthView', () => {
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}},
{provide: MAT_CALENDAR_RANGE_SELECTION_STRATEGY, useClass: DefaultMatCalendarRangeStrategy}
{provide: MAT_DATE_RANGE_SELECTION_STRATEGY, useClass: DefaultMatCalendarRangeStrategy}
]
});

Expand Down
10 changes: 5 additions & 5 deletions src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ import {Subscription} from 'rxjs';
import {startWith} from 'rxjs/operators';
import {DateRange} from './date-selection-model';
import {
MatCalendarRangeSelectionStrategy,
MAT_CALENDAR_RANGE_SELECTION_STRATEGY,
} from './calendar-range-selection-strategy';
MatDateRangeSelectionStrategy,
MAT_DATE_RANGE_SELECTION_STRATEGY,
} from './date-range-selection-strategy';


const DAYS_PER_WEEK = 7;
Expand Down Expand Up @@ -179,8 +179,8 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() private _dir?: Directionality,
@Inject(MAT_CALENDAR_RANGE_SELECTION_STRATEGY) @Optional()
private _rangeStrategy?: MatCalendarRangeSelectionStrategy<D>) {
@Inject(MAT_DATE_RANGE_SELECTION_STRATEGY) @Optional()
private _rangeStrategy?: MatDateRangeSelectionStrategy<D>) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
Expand Down
2 changes: 1 addition & 1 deletion src/material/datepicker/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export * from './datepicker-module';
export * from './calendar';
export * from './calendar-body';
export * from './datepicker';
export * from './calendar-range-selection-strategy';
export * from './date-range-selection-strategy';
export * from './datepicker-animations';
export {
MAT_DATEPICKER_SCROLL_STRATEGY,
Expand Down
Loading