Skip to content

Commit d062ccb

Browse files
julianobrasilmmalerba
authored andcommitted
fix(datepicker): add max/min filter to multi year and year views (#9727)
* add max/min filter to multi year and year views * address @mmalerba's comments * fix and tighten up logic * possibility to have a null filter in month view
1 parent c2e108e commit d062ccb

File tree

5 files changed

+125
-26
lines changed

5 files changed

+125
-26
lines changed

src/lib/datepicker/calendar.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
*ngSwitchCase="'month'"
2727
[activeDate]="_activeDate"
2828
[selected]="selected"
29-
[dateFilter]="_dateFilterForViews"
29+
[dateFilter]="dateFilter"
30+
[maxDate]="maxDate"
31+
[minDate]="minDate"
3032
(selectedChange)="_dateSelected($event)"
3133
(_userSelection)="_userSelected()">
3234
</mat-month-view>
@@ -35,7 +37,9 @@
3537
*ngSwitchCase="'year'"
3638
[activeDate]="_activeDate"
3739
[selected]="selected"
38-
[dateFilter]="_dateFilterForViews"
40+
[dateFilter]="dateFilter"
41+
[maxDate]="maxDate"
42+
[minDate]="minDate"
3943
(monthSelected)="_monthSelectedInYearView($event)"
4044
(selectedChange)="_goToDateInView($event, 'month')">
4145
</mat-year-view>
@@ -44,7 +48,9 @@
4448
*ngSwitchCase="'multi-year'"
4549
[activeDate]="_activeDate"
4650
[selected]="selected"
47-
[dateFilter]="_dateFilterForViews"
51+
[dateFilter]="dateFilter"
52+
[maxDate]="maxDate"
53+
[minDate]="minDate"
4854
(yearSelected)="_yearSelectedInMultiYearView($event)"
4955
(selectedChange)="_goToDateInView($event, 'year')">
5056
</mat-multi-year-view>

src/lib/datepicker/calendar.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,6 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
131131
/** Reference to the current multi-year view component. */
132132
@ViewChild(MatMultiYearView) multiYearView: MatMultiYearView<D>;
133133

134-
/** Date filter for the month, year, and multi-year views. */
135-
_dateFilterForViews = (date: D) => {
136-
return !!date &&
137-
(!this.dateFilter || this.dateFilter(date)) &&
138-
(!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) &&
139-
(!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0);
140-
}
141-
142134
/**
143135
* The current active date. This determines which time period is shown and which date is
144136
* highlighted when using keyboard navigation.
@@ -368,7 +360,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
368360
this._dateAdapter.addCalendarMonths(this._activeDate, 1);
369361
break;
370362
case ENTER:
371-
if (this._dateFilterForViews(this._activeDate)) {
363+
if (!this.dateFilter || this.dateFilter(this._activeDate)) {
372364
this._dateSelected(this._activeDate);
373365
this._userSelected();
374366
// Prevent unexpected default actions such as form submission.

src/lib/datepicker/month-view.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ export class MatMonthView<D> implements AfterContentInit {
6464
}
6565
private _selected: D | null;
6666

67+
/** The minimum selectable date. */
68+
@Input()
69+
get minDate(): D | null { return this._minDate; }
70+
set minDate(value: D | null) {
71+
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
72+
}
73+
private _minDate: D | null;
74+
75+
/** The maximum selectable date. */
76+
@Input()
77+
get maxDate(): D | null { return this._maxDate; }
78+
set maxDate(value: D | null) {
79+
this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
80+
}
81+
private _maxDate: D | null;
82+
6783
/** A function used to filter which dates are selectable. */
6884
@Input() dateFilter: (date: D) => boolean;
6985

@@ -154,25 +170,32 @@ export class MatMonthView<D> implements AfterContentInit {
154170

155171
/** Creates MatCalendarCells for the dates in this month. */
156172
private _createWeekCells() {
157-
let daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
158-
let dateNames = this._dateAdapter.getDateNames();
173+
const daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
174+
const dateNames = this._dateAdapter.getDateNames();
159175
this._weeks = [[]];
160176
for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {
161177
if (cell == DAYS_PER_WEEK) {
162178
this._weeks.push([]);
163179
cell = 0;
164180
}
165-
let date = this._dateAdapter.createDate(
166-
this._dateAdapter.getYear(this.activeDate),
167-
this._dateAdapter.getMonth(this.activeDate), i + 1);
168-
let enabled = !this.dateFilter ||
169-
this.dateFilter(date);
170-
let ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
181+
const date = this._dateAdapter.createDate(
182+
this._dateAdapter.getYear(this.activeDate),
183+
this._dateAdapter.getMonth(this.activeDate), i + 1);
184+
const enabled = this._shouldEnableDate(date);
185+
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
171186
this._weeks[this._weeks.length - 1]
172187
.push(new MatCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
173188
}
174189
}
175190

191+
/** Date filter for the month */
192+
private _shouldEnableDate(date: D): boolean {
193+
return !!date &&
194+
(!this.dateFilter || this.dateFilter(date)) &&
195+
(!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) &&
196+
(!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0);
197+
}
198+
176199
/**
177200
* Gets the date in this month that the given Date falls on.
178201
* Returns null if the given Date is in another month.

src/lib/datepicker/multi-year-view.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ export class MatMultiYearView<D> implements AfterContentInit {
6464
}
6565
private _selected: D | null;
6666

67+
/** The minimum selectable date. */
68+
@Input()
69+
get minDate(): D | null { return this._minDate; }
70+
set minDate(value: D | null) {
71+
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
72+
}
73+
private _minDate: D | null;
74+
75+
/** The maximum selectable date. */
76+
@Input()
77+
get maxDate(): D | null { return this._maxDate; }
78+
set maxDate(value: D | null) {
79+
this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
80+
}
81+
private _maxDate: D | null;
82+
6783
/** A function used to filter which dates are selectable. */
6884
@Input() dateFilter: (date: D) => boolean;
6985

@@ -128,11 +144,19 @@ export class MatMultiYearView<D> implements AfterContentInit {
128144
/** Creates an MatCalendarCell for the given year. */
129145
private _createCellForYear(year: number) {
130146
let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1));
131-
return new MatCalendarCell(year, yearName, yearName, this._isYearEnabled(year));
147+
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));
132148
}
133149

134150
/** Whether the given year is enabled. */
135-
private _isYearEnabled(year: number) {
151+
private _shouldEnableYear(year: number) {
152+
// disable if the year is greater than maxDate lower than minDate
153+
if (year === undefined || year === null ||
154+
(this.maxDate && year > this._dateAdapter.getYear(this.maxDate)) ||
155+
(this.minDate && year < this._dateAdapter.getYear(this.minDate))) {
156+
return false;
157+
}
158+
159+
// enable if it reaches here and there's no filter defined
136160
if (!this.dateFilter) {
137161
return true;
138162
}

src/lib/datepicker/year-view.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ export class MatYearView<D> implements AfterContentInit {
5959
}
6060
private _selected: D | null;
6161

62+
/** The minimum selectable date. */
63+
@Input()
64+
get minDate(): D | null { return this._minDate; }
65+
set minDate(value: D | null) {
66+
this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
67+
}
68+
private _minDate: D | null;
69+
70+
/** The maximum selectable date. */
71+
@Input()
72+
get maxDate(): D | null { return this._maxDate; }
73+
set maxDate(value: D | null) {
74+
this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
75+
}
76+
private _maxDate: D | null;
77+
6278
/** A function used to filter which dates are selectable. */
6379
@Input() dateFilter: (date: D) => boolean;
6480

@@ -142,17 +158,25 @@ export class MatYearView<D> implements AfterContentInit {
142158
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
143159
this._dateFormats.display.monthYearA11yLabel);
144160
return new MatCalendarCell(
145-
month, monthName.toLocaleUpperCase(), ariaLabel, this._isMonthEnabled(month));
161+
month, monthName.toLocaleUpperCase(), ariaLabel, this._shouldEnableMonth(month));
146162
}
147163

148164
/** Whether the given month is enabled. */
149-
private _isMonthEnabled(month: number) {
165+
private _shouldEnableMonth(month: number) {
166+
167+
const activeYear = this._dateAdapter.getYear(this.activeDate);
168+
169+
if (month === undefined || month === null ||
170+
this._isYearAndMonthAfterMaxDate(activeYear, month) ||
171+
this._isYearAndMonthBeforeMinDate(activeYear, month)) {
172+
return false;
173+
}
174+
150175
if (!this.dateFilter) {
151176
return true;
152177
}
153178

154-
let firstOfMonth = this._dateAdapter.createDate(
155-
this._dateAdapter.getYear(this.activeDate), month, 1);
179+
const firstOfMonth = this._dateAdapter.createDate(activeYear, month, 1);
156180

157181
// If any date in the month is enabled count the month as enabled.
158182
for (let date = firstOfMonth; this._dateAdapter.getMonth(date) == month;
@@ -165,6 +189,36 @@ export class MatYearView<D> implements AfterContentInit {
165189
return false;
166190
}
167191

192+
/**
193+
* Tests whether the combination month/year is after this.maxDate, considering
194+
* just the month and year of this.maxDate
195+
*/
196+
private _isYearAndMonthAfterMaxDate(year: number, month: number) {
197+
if (this.maxDate) {
198+
const maxYear = this._dateAdapter.getYear(this.maxDate);
199+
const maxMonth = this._dateAdapter.getMonth(this.maxDate);
200+
201+
return year > maxYear || (year === maxYear && month > maxMonth);
202+
}
203+
204+
return false;
205+
}
206+
207+
/**
208+
* Tests whether the combination month/year is before this.minDate, considering
209+
* just the month and year of this.minDate
210+
*/
211+
private _isYearAndMonthBeforeMinDate(year: number, month: number) {
212+
if (this.minDate) {
213+
const minYear = this._dateAdapter.getYear(this.minDate);
214+
const minMonth = this._dateAdapter.getMonth(this.minDate);
215+
216+
return year < minYear || (year === minYear && month < minMonth);
217+
}
218+
219+
return false;
220+
}
221+
168222
/**
169223
* @param obj The object to check.
170224
* @returns The given object if it is both a date instance and valid, otherwise null.

0 commit comments

Comments
 (0)