Skip to content

Commit 9aeeb76

Browse files
roboshoesmmalerba
authored andcommitted
Test fixes for date-range using selection model (#13919)
* adds an abstract date selection model and a single date and range implementation * moves adapter to contructor * adds asDate and asRange convertions * adds explicit constructors to concrete implementations * Adds an abstract date selection model (#13033) This PR also includes a single date implementation of the selection model and the range implementation. * WIP uses date selectio in calendar * Observable in MatDateSelection (#13037) This change adds an observable to the date selection. This allows the usage of a single date selection instance instead of creating multiple. * test fixes * small cleanup * cleanup * improves calendar body test spec * fixes tests in month view * fixes matCalendar tests * cleared up calendar body tests with date injection * indent * cleaning up some nits * fixes the final tests * update today programatically * remove unused file * nits
1 parent 17817d9 commit 9aeeb76

File tree

12 files changed

+98
-46
lines changed

12 files changed

+98
-46
lines changed

src/dev-app/tsconfig-aot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// TypeScript config that extends the dev-app tsconfig file. This config compiles the
2-
// "main-aot.ts" file and also enables templage code generation / AOT.
2+
// "main-aot.ts" file and also enables template code generation / AOT.
33
{
44
"extends": "./tsconfig-build",
55
"compilerOptions": {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ export class MatSingleDateSelectionModel<D> extends MatDateSelectionModel<D> {
8383
* simply replacing the current selection with the given selection.
8484
*/
8585
add(date: D) {
86-
this.date = date;
87-
this.selectionChange.next();
86+
if (!this.adapter.sameDate(date, this.date)) {
87+
this.date = date;
88+
this.selectionChange.next();
89+
}
8890
}
8991

9092
clone(): MatDateSelectionModel<D> {

src/lib/datepicker/calendar-body.spec.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import {Component} from '@angular/core';
2-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
33
import {By} from '@angular/platform-browser';
4+
import {MatNativeDateModule, DateAdapter} from '@angular/material/core';
45
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
56

67

78
describe('MatCalendarBody', () => {
89
beforeEach(async(() => {
910
TestBed.configureTestingModule({
11+
imports: [
12+
MatNativeDateModule,
13+
],
1014
declarations: [
1115
MatCalendarBody,
1216

@@ -32,7 +36,10 @@ describe('MatCalendarBody', () => {
3236
cellEls = Array.from(calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell'));
3337
}
3438

35-
beforeEach(() => {
39+
beforeEach(inject([DateAdapter], (adapter: DateAdapter<Date>) => {
40+
const fakeToday = new Date(2017, 0, 3);
41+
spyOn(adapter, 'today').and.callFake(() => fakeToday);
42+
3643
fixture = TestBed.createComponent(StandardCalendarBody);
3744
fixture.detectChanges();
3845

@@ -41,7 +48,7 @@ describe('MatCalendarBody', () => {
4148
testComponent = fixture.componentInstance;
4249

4350
refreshElementLists();
44-
});
51+
}));
4552

4653
it('creates body', () => {
4754
expect(rowEls.length).toBe(3);
@@ -140,7 +147,7 @@ class StandardCalendarBody {
140147

141148
function createCell(value: number, cellClasses?: MatCalendarCellCssClasses) {
142149
return new MatCalendarCell(
143-
{start: new Date(value), end: new Date(value)},
150+
{start: new Date(2017, 0, value), end: new Date(2017, 0, value)},
144151
`${value}`,
145152
`${value}-label`,
146153
true,

src/lib/datepicker/calendar-body.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
ViewEncapsulation,
1818
OnChanges,
1919
SimpleChanges,
20+
ChangeDetectorRef,
21+
OnDestroy,
2022
} from '@angular/core';
2123
import {
2224
DateAdapter,
@@ -25,6 +27,7 @@ import {
2527
MatSingleDateSelectionModel
2628
} from '@angular/material/core';
2729
import {take} from 'rxjs/operators';
30+
import {Subscription} from 'rxjs';
2831

2932
/**
3033
* Extra CSS classes that can be associated with a calendar cell.
@@ -71,7 +74,7 @@ export class MatCalendarCell<D = unknown> {
7174
providers: [MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER],
7275
})
7376
// @breaking-change 9.0.0 remove generic default type
74-
export class MatCalendarBody<D = unknown> implements OnChanges {
77+
export class MatCalendarBody<D = unknown> implements OnChanges, OnDestroy {
7578
/** The label for the table. (e.g. "Jan 2017"). */
7679
@Input() label: string;
7780

@@ -154,18 +157,21 @@ export class MatCalendarBody<D = unknown> implements OnChanges {
154157
_cellWidth: string;
155158

156159
private _today: D;
160+
private _selectionSubscription: Subscription;
157161

158162
constructor(private _elementRef: ElementRef<HTMLElement>,
159163
private _ngZone: NgZone,
164+
private _cdr: ChangeDetectorRef,
160165
private _dateAdapter: DateAdapter<D>,
161166
readonly _selectionModel: MatDateSelectionModel<D>) {
162-
this._today = this._dateAdapter.today();
163-
// Note(mmalerba): This is required to zero out the time portion of the date.
164-
// Revisit this when we support time picking.
165-
this._today = this._dateAdapter.createDate(
166-
this._dateAdapter.getYear(this._today),
167-
this._dateAdapter.getMonth(this._today),
168-
this._dateAdapter.getDate(this._today));
167+
this._updateToday();
168+
169+
this._selectionSubscription =
170+
this._selectionModel.selectionChange.subscribe(() => this._cdr.markForCheck());
171+
}
172+
173+
ngOnDestroy() {
174+
this._selectionSubscription.unsubscribe();
169175
}
170176

171177
_cellClicked(cell: MatCalendarCell<D>): void {
@@ -215,9 +221,8 @@ export class MatCalendarBody<D = unknown> implements OnChanges {
215221
}
216222

217223
_isToday(item: MatCalendarCell<D>): boolean {
218-
const today = this._dateAdapter.today();
219-
return this._dateAdapter.compareDate(item.range.start, today) <= 0 &&
220-
this._dateAdapter.compareDate(item.range.end, today) >= 0;
224+
return this._dateAdapter.compareDate(item.range.start, this._today) <= 0 &&
225+
this._dateAdapter.compareDate(item.range.end, this._today) >= 0;
221226
}
222227

223228
/** Focuses the active cell after the microtask queue is empty. */
@@ -234,6 +239,16 @@ export class MatCalendarBody<D = unknown> implements OnChanges {
234239
});
235240
}
236241

242+
_updateToday() {
243+
this._today = this._dateAdapter.today();
244+
// Note(mmalerba): This is required to zero out the time portion of the date.
245+
// Revisit this when we support time picking.
246+
this._today = this._dateAdapter.createDate(
247+
this._dateAdapter.getYear(this._today),
248+
this._dateAdapter.getMonth(this._today),
249+
this._dateAdapter.getDate(this._today));
250+
}
251+
237252
// @breaking-change 9.0.0 remove when deprecated properties relying on it are removed.
238253
private _getFirstCellRange() {
239254
return (this.rows && this.rows[0] && this.rows[0][0] && this.rows[0][0].range);

src/lib/datepicker/calendar.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
186186
_calendarHeaderPortal: Portal<any>;
187187

188188
private _intlChanges: Subscription;
189+
private _selectionSubscription: Subscription;
189190

190191
/**
191192
* Used for scheduling that focus should be moved to the active cell on the next tick.
@@ -240,7 +241,11 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
240241
/** Function that can be used to add custom CSS classes to dates. */
241242
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
242243

243-
/** Emits when the currently selected date changes. */
244+
/**
245+
* Emits when the currently selected date changes.
246+
* @deprecated Listen to selectionModel valueChange.
247+
* @breaking-change 9.0.0
248+
*/
244249
@Output() readonly selectedChange = new EventEmitter<D>();
245250

246251
/**
@@ -309,13 +314,18 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
309314
_changeDetectorRef.markForCheck();
310315
this.stateChanges.next();
311316
});
317+
318+
// This should no longer be needed after deprecation of selectedChange
319+
this._selectionSubscription = selectionModel.selectionChange.subscribe(() => {
320+
this.selectedChange.emit(selectionModel.getFirstSelectedDate() || undefined);
321+
});
312322
}
313323

314324
ngAfterContentInit() {
315325
this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);
316326
this.activeDate = this.startAt || this._dateAdapter.today();
317327

318-
// Assign to the private property since we don't want to move focus on init.
328+
// Assign to the private property since we don't want to move on init.
319329
this._currentView = this.startView;
320330
}
321331

@@ -328,6 +338,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
328338

329339
ngOnDestroy() {
330340
this._intlChanges.unsubscribe();
341+
this._selectionSubscription.unsubscribe();
331342
this.stateChanges.complete();
332343
}
333344

src/lib/datepicker/datepicker-content.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
[headerComponent]="datepicker.calendarHeaderComponent"
1010
[dateClass]="datepicker.dateClass"
1111
[@fadeInCalendar]="'enter'"
12-
(selectedChange)="datepicker.select($event)"
1312
(yearSelected)="datepicker._selectYear($event)"
1413
(monthSelected)="datepicker._selectMonth($event)"
1514
(_userSelection)="datepicker.close()">

src/lib/datepicker/datepicker-input.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,19 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
109109
this._datepicker = value;
110110
this._datepicker._registerInput(this);
111111
this._datepickerSubscription.unsubscribe();
112+
113+
if (this._isSelectionInitialized) {
114+
this._isSelectionInitialized = false;
115+
this._selectionModel.ngOnDestroy();
116+
}
117+
112118
this._selectionModel = this._datepicker._dateSelection;
113119

114-
this._formatValue(this._selectionModel.getFirstSelectedDate());
120+
this._formatValue(this._selectionModel.getSelection());
115121

116122
this._datepickerSubscription = this._datepicker._dateSelection.selectionChange.subscribe(() => {
117-
this._formatValue(this._selectionModel.getFirstSelectedDate());
118-
this._cvaOnChange(this._selectionModel.getFirstSelectedDate());
123+
this._formatValue(this._selectionModel.getSelection());
124+
this._cvaOnChange(this._selectionModel.getSelection());
119125
this._onTouched();
120126
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
121127
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
@@ -134,22 +140,23 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
134140
/** The value of the input. */
135141
@Input()
136142
get value(): D | null {
137-
return this._selectionModel ? this._selectionModel.getFirstSelectedDate() : null;
143+
return this._selectionModel ? this._selectionModel.getSelection() : null;
138144
}
139145
set value(value: D | null) {
140-
const oldDate = this._selectionModel.getFirstSelectedDate();
146+
value = this._dateAdapter.deserialize(value);
147+
const oldDate = this._selectionModel.getSelection();
141148

142149
if (!this._selectionModel) {
143150
throw new Error('Input has no MatDatePicker associated with it.');
144151
}
145152

146-
if (!this._dateAdapter.sameDate(value, this._selectionModel.getFirstSelectedDate())) {
153+
if (!this._dateAdapter.sameDate(value, oldDate)) {
147154
this._selectionModel.setSelection(value);
148155
}
149156

150-
this._lastValueValid = !this._selectionModel || this._selectionModel.isValid();
157+
this._lastValueValid = this._selectionModel.isValid();
151158

152-
this._formatValue(this._selectionModel.getFirstSelectedDate());
159+
this._formatValue(this._selectionModel.getSelection());
153160

154161
if (!this._dateAdapter.sameDate(value, oldDate)) {
155162
this._valueChange.emit(value);
@@ -221,6 +228,8 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
221228

222229
private _localeSubscription = Subscription.EMPTY;
223230

231+
private _isSelectionInitialized = true;
232+
224233
/** The form control validator for whether the input parses. */
225234
private _parseValidator: ValidatorFn = (): ValidationErrors | null => {
226235
return this._lastValueValid ?
@@ -274,6 +283,10 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
274283
this._localeSubscription = _dateAdapter.localeChanges.subscribe(() => {
275284
this.value = this.value;
276285
});
286+
287+
// Set a default model to prevent failure when reading value. Gets overridden when the
288+
// datepicker is set.
289+
this._selectionModel = new MatSingleDateSelectionModel(_dateAdapter);
277290
}
278291

279292
ngOnDestroy() {
@@ -339,17 +352,14 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
339352
}
340353

341354
_onInput(value: string) {
342-
const date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
343-
const current = this._selectionModel.getFirstSelectedDate();
344-
345-
if (!date) {
346-
return;
347-
}
355+
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
356+
const current = this._selectionModel.getSelection();
357+
date = this._getValidDateOrNull(date);
348358

349359
if (!this._dateAdapter.sameDate(current, date)) {
350-
this._selectionModel.add(date);
351-
this._cvaOnChange(this._selectionModel.getFirstSelectedDate());
352-
this._valueChange.emit(this._selectionModel.getFirstSelectedDate());
360+
this._selectionModel.setSelection(date);
361+
this._formatValue(date);
362+
this._cvaOnChange(date);
353363
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
354364
}
355365
}
@@ -380,7 +390,8 @@ export class MatDatepickerInput<D> implements ControlValueAccessor, OnDestroy, V
380390
}
381391

382392
this._elementRef.nativeElement.value =
383-
value ? this._dateAdapter.format(value, this._dateFormats.display.dateInput) : '';
393+
value && this._getValidDateOrNull(value) ?
394+
this._dateAdapter.format(value, this._dateFormats.display.dateInput) : '';
384395
}
385396

386397
/**

src/lib/datepicker/datepicker.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ describe('MatDatepicker', () => {
654654
fakeAsync(() => {
655655
expect(testComponent.onMultiYearSelection).not.toHaveBeenCalled();
656656

657+
657658
testComponent.datepicker.open();
658659
fixture.detectChanges();
659660

src/lib/datepicker/datepicker.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
273273
private _focusedElementBeforeOpen: HTMLElement | null = null;
274274

275275
/** Subscription to value changes in the associated input element. */
276-
private _inputSubscription = Subscription.EMPTY;
276+
private _subscriptions = new Subscription();
277277

278278
/** The input element this datepicker is associated with. */
279279
_datepickerInput: MatDatepickerInput<D>;
@@ -299,11 +299,15 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
299299
}
300300

301301
this._scrollStrategy = scrollStrategy;
302+
303+
this._subscriptions.add(_dateSelection.selectionChange.subscribe(() => {
304+
this._selectedChanged.next(_dateSelection.getSelection() || undefined);
305+
}));
302306
}
303307

304308
ngOnDestroy() {
305309
this.close();
306-
this._inputSubscription.unsubscribe();
310+
this._subscriptions.unsubscribe();
307311
this._disabledChange.complete();
308312

309313
if (this._popupRef) {
@@ -315,9 +319,8 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
315319
/** Selects the given date */
316320
select(date: D): void {
317321
let oldValue = this._dateSelection.getSelection();
318-
if (!this._dateAdapter.sameDate(oldValue, this._dateSelection.getSelection())) {
322+
if (!this._dateAdapter.sameDate(oldValue, date)) {
319323
this._dateSelection.add(date);
320-
this._selectedChanged.next(this._dateSelection.getSelection() || undefined);
321324
}
322325
}
323326

src/lib/datepicker/month-view.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
8888
get selected(): D | null { return this._selectionModel.getFirstSelectedDate(); }
8989
set selected(value: D | null) {
9090
if (this._selectionModel instanceof MatSingleDateSelectionModel) {
91-
this._selectionModel.setSelection(value);
91+
this._selectionModel.add(value);
9292
this.extractDate();
9393
}
9494
}
@@ -179,6 +179,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
179179
}
180180

181181
ngAfterContentInit() {
182+
this._matCalendarBody._updateToday();
182183
this._init();
183184
}
184185

0 commit comments

Comments
 (0)