Skip to content

Commit 7870e38

Browse files
committed
fix(datepicker): support dateClass on year and multi-year views
We have a `dateClass` input that allows consumers to set classes to particular date cells, however currently we only invoke it inside the month view. These changes pass it along to the year and multi-year views too and provide the name of the view that invoked it so that cases like the first date of a month and a month cell can be distinguished. Fixes #20017.
1 parent 25ce323 commit 7870e38

File tree

11 files changed

+127
-28
lines changed

11 files changed

+127
-28
lines changed
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, ViewEncapsulation} from '@angular/core';
2-
import {MatCalendarCellCssClasses} from '@angular/material/datepicker';
2+
import {MatCalendarCellClassFunction} from '@angular/material/datepicker';
33

44
/** @title Datepicker with custom date classes */
55
@Component({
@@ -9,10 +9,15 @@ import {MatCalendarCellCssClasses} from '@angular/material/datepicker';
99
encapsulation: ViewEncapsulation.None,
1010
})
1111
export class DatepickerDateClassExample {
12-
dateClass = (d: Date): MatCalendarCellCssClasses => {
13-
const date = d.getDate();
12+
dateClass: MatCalendarCellClassFunction<Date> = (cellDate, view) => {
13+
// Only highligh dates inside the month view.
14+
if (view === 'month') {
15+
const date = cellDate.getDate();
1416

15-
// Highlight the 1st and 20th day of each month.
16-
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
17+
// Highlight the 1st and 20th day of each month.
18+
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
19+
}
20+
21+
return '';
1722
}
1823
}

src/material/datepicker/calendar-body.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import {
2121
} from '@angular/core';
2222
import {take} from 'rxjs/operators';
2323

24-
/**
25-
* Extra CSS classes that can be associated with a calendar cell.
26-
*/
24+
/** Extra CSS classes that can be associated with a calendar cell. */
2725
export type MatCalendarCellCssClasses = string | string[] | Set<string> | {[key: string]: any};
2826

27+
/** Function that can generate the extra classes that should be added to a calendar cell. */
28+
export type MatCalendarCellClassFunction<D> =
29+
(date: D, view: 'month' | 'year' | 'multi-year') => MatCalendarCellCssClasses;
30+
2931
/**
3032
* An internal class that represents the data corresponding to a single calendar cell.
3133
* @docs-private

src/material/datepicker/calendar.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
[dateFilter]="dateFilter"
2222
[maxDate]="maxDate"
2323
[minDate]="minDate"
24+
[dateClass]="dateClass"
2425
(monthSelected)="_monthSelectedInYearView($event)"
2526
(selectedChange)="_goToDateInView($event, 'month')">
2627
</mat-year-view>
@@ -32,6 +33,7 @@
3233
[dateFilter]="dateFilter"
3334
[maxDate]="maxDate"
3435
[minDate]="minDate"
36+
[dateClass]="dateClass"
3537
(yearSelected)="_yearSelectedInMultiYearView($event)"
3638
(selectedChange)="_goToDateInView($event, 'year')">
3739
</mat-multi-year-view>

src/material/datepicker/calendar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
MatDateFormats,
3232
} from '@angular/material/core';
3333
import {Subject, Subscription} from 'rxjs';
34-
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
34+
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
3535
import {createMissingDateImplError} from './datepicker-errors';
3636
import {MatDatepickerIntl} from './datepicker-intl';
3737
import {MatMonthView} from './month-view';
@@ -245,7 +245,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
245245
@Input() dateFilter: (date: D) => boolean;
246246

247247
/** Function that can be used to add custom CSS classes to dates. */
248-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
248+
@Input() dateClass: MatCalendarCellClassFunction<D>;
249249

250250
/** Start of the comparison range. */
251251
@Input() comparisonStart: D | null;

src/material/datepicker/datepicker-base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ 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, MatCalendarUserEvent} from './calendar-body';
56+
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
5757
import {DateFilterFn} from './datepicker-input-base';
5858
import {
5959
ExtractDateTypeFromSelection,
@@ -322,7 +322,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
322322
@Input() panelClass: string | string[];
323323

324324
/** Function that can be used to add custom CSS classes to dates. */
325-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
325+
@Input() dateClass: MatCalendarCellClassFunction<D>;
326326

327327
/** Emits when the datepicker has been opened. */
328328
@Output('opened') openedStream: EventEmitter<void> = new EventEmitter<void>();

src/material/datepicker/month-view.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import {Directionality} from '@angular/cdk/bidi';
3838
import {
3939
MatCalendarBody,
4040
MatCalendarCell,
41-
MatCalendarCellCssClasses,
4241
MatCalendarUserEvent,
42+
MatCalendarCellClassFunction,
4343
} from './calendar-body';
4444
import {createMissingDateImplError} from './datepicker-errors';
4545
import {Subscription} from 'rxjs';
@@ -120,7 +120,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
120120
@Input() dateFilter: (date: D) => boolean;
121121

122122
/** Function that can be used to add custom CSS classes to dates. */
123-
@Input() dateClass: (date: D) => MatCalendarCellCssClasses;
123+
@Input() dateClass: MatCalendarCellClassFunction<D>;
124124

125125
/** Start of the comparison range. */
126126
@Input() comparisonStart: D | null;
@@ -370,7 +370,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
370370
this._dateAdapter.getMonth(this.activeDate), i + 1);
371371
const enabled = this._shouldEnableDate(date);
372372
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
373-
const cellClasses = this.dateClass ? this.dateClass(date) : undefined;
373+
const cellClasses = this.dateClass ? this.dateClass(date, 'month') : undefined;
374374

375375
this._weeks[this._weeks.length - 1].push(new MatCalendarCell<D>(i + 1, dateNames[i],
376376
ariaLabel, enabled, cellClasses, this._getCellCompareValue(date)!, date));

src/material/datepicker/multi-year-view.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('MatMultiYearView', () => {
3434
StandardMultiYearView,
3535
MultiYearViewWithDateFilter,
3636
MultiYearViewWithMinMaxDate,
37+
MultiYearViewWithDateClass,
3738
],
3839
providers: [
3940
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
@@ -343,6 +344,26 @@ describe('MatMultiYearView', () => {
343344
expect(cells[9].classList).not.toContain('mat-calendar-body-disabled');
344345
});
345346
});
347+
348+
describe('multi-year view with custom date classes', () => {
349+
let fixture: ComponentFixture<MultiYearViewWithDateClass>;
350+
let multiYearViewNativeElement: Element;
351+
352+
beforeEach(() => {
353+
fixture = TestBed.createComponent(MultiYearViewWithDateClass);
354+
fixture.detectChanges();
355+
356+
let multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView))!;
357+
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
358+
});
359+
360+
it('should be able to add a custom class to some dates', () => {
361+
let cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
362+
expect(cells[0].classList).toContain('even');
363+
expect(cells[1].classList).not.toContain('even');
364+
});
365+
});
366+
346367
});
347368

348369
@Component({
@@ -387,3 +408,16 @@ class MultiYearViewWithMinMaxDate {
387408
minDate: Date | null;
388409
maxDate: Date | null;
389410
}
411+
412+
413+
@Component({
414+
template: `
415+
<mat-multi-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-multi-year-view>
416+
`
417+
})
418+
class MultiYearViewWithDateClass {
419+
activeDate = new Date(2017, JAN, 1);
420+
dateClass(date: Date) {
421+
return date.getFullYear() % 2 == 0 ? 'even' : undefined;
422+
}
423+
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
} from '@angular/core';
3434
import {DateAdapter} from '@angular/material/core';
3535
import {Directionality} from '@angular/cdk/bidi';
36-
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
36+
import {
37+
MatCalendarBody,
38+
MatCalendarCell,
39+
MatCalendarUserEvent,
40+
MatCalendarCellClassFunction,
41+
} from './calendar-body';
3742
import {createMissingDateImplError} from './datepicker-errors';
3843
import {Subscription} from 'rxjs';
3944
import {startWith} from 'rxjs/operators';
@@ -109,6 +114,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
109114
/** A function used to filter which dates are selectable. */
110115
@Input() dateFilter: (date: D) => boolean;
111116

117+
/** Function that can be used to add custom CSS classes to date cells. */
118+
@Input() dateClass: MatCalendarCellClassFunction<D>;
119+
112120
/** Emits when a new year is selected. */
113121
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
114122

@@ -251,8 +259,11 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
251259

252260
/** Creates an MatCalendarCell for the given year. */
253261
private _createCellForYear(year: number) {
254-
let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1));
255-
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));
262+
const date = this._dateAdapter.createDate(year, 0, 1);
263+
const yearName = this._dateAdapter.getYearName(date);
264+
const cellClasses = this.dateClass ? this.dateClass(date, 'year') : undefined;
265+
266+
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year), cellClasses);
256267
}
257268

258269
/** Whether the given year is enabled. */

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('MatYearView', () => {
3333
// Test components.
3434
StandardYearView,
3535
YearViewWithDateFilter,
36+
YearViewWithDateClass,
3637
],
3738
providers: [
3839
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
@@ -329,6 +330,26 @@ describe('MatYearView', () => {
329330
});
330331

331332
});
333+
334+
describe('year view with custom date classes', () => {
335+
let fixture: ComponentFixture<YearViewWithDateClass>;
336+
let yearViewNativeElement: Element;
337+
338+
beforeEach(() => {
339+
fixture = TestBed.createComponent(YearViewWithDateClass);
340+
fixture.detectChanges();
341+
342+
let yearViewDebugElement = fixture.debugElement.query(By.directive(MatYearView))!;
343+
yearViewNativeElement = yearViewDebugElement.nativeElement;
344+
});
345+
346+
it('should be able to add a custom class to some month cells', () => {
347+
let cells = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
348+
expect(cells[0].classList).toContain('even');
349+
expect(cells[1].classList).not.toContain('even');
350+
});
351+
});
352+
332353
});
333354

334355

@@ -368,3 +389,14 @@ class YearViewWithDateFilter {
368389
return true;
369390
}
370391
}
392+
393+
394+
@Component({
395+
template: `<mat-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-year-view>`
396+
})
397+
class YearViewWithDateClass {
398+
activeDate = new Date(2017, JAN, 1);
399+
dateClass(date: Date) {
400+
return date.getMonth() % 2 == 0 ? 'even' : undefined;
401+
}
402+
}

src/material/datepicker/year-view.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ import {
3434
} from '@angular/core';
3535
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
3636
import {Directionality} from '@angular/cdk/bidi';
37-
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
37+
import {
38+
MatCalendarBody,
39+
MatCalendarCell,
40+
MatCalendarUserEvent,
41+
MatCalendarCellClassFunction,
42+
} from './calendar-body';
3843
import {createMissingDateImplError} from './datepicker-errors';
3944
import {Subscription} from 'rxjs';
4045
import {startWith} from 'rxjs/operators';
@@ -103,6 +108,9 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
103108
/** A function used to filter which dates are selectable. */
104109
@Input() dateFilter: (date: D) => boolean;
105110

111+
/** Function that can be used to add custom CSS classes to date cells. */
112+
@Input() dateClass: MatCalendarCellClassFunction<D>;
113+
106114
/** Emits when a new month is selected. */
107115
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
108116

@@ -254,11 +262,12 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
254262

255263
/** Creates an MatCalendarCell for the given month. */
256264
private _createCellForMonth(month: number, monthName: string) {
257-
let ariaLabel = this._dateAdapter.format(
258-
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
259-
this._dateFormats.display.monthYearA11yLabel);
260-
return new MatCalendarCell(
261-
month, monthName.toLocaleUpperCase(), ariaLabel, this._shouldEnableMonth(month));
265+
const date = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1);
266+
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.monthYearA11yLabel);
267+
const cellClasses = this.dateClass ? this.dateClass(date, 'year') : undefined;
268+
269+
return new MatCalendarCell(month, monthName.toLocaleUpperCase(), ariaLabel,
270+
this._shouldEnableMonth(month), cellClasses);
262271
}
263272

264273
/** Whether the given month is enabled. */

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export declare class MatCalendar<D> implements AfterContentInit, AfterViewChecke
6060
comparisonStart: D | null;
6161
get currentView(): MatCalendarView;
6262
set currentView(value: MatCalendarView);
63-
dateClass: (date: D) => MatCalendarCellCssClasses;
63+
dateClass: MatCalendarCellClassFunction<D>;
6464
dateFilter: (date: D) => boolean;
6565
headerComponent: ComponentType<any>;
6666
get maxDate(): D | null;
@@ -147,6 +147,8 @@ export declare class MatCalendarCell<D = any> {
147147
constructor(value: number, displayValue: string, ariaLabel: string, enabled: boolean, cssClasses?: MatCalendarCellCssClasses, compareValue?: number, rawValue?: D | undefined);
148148
}
149149

150+
export declare type MatCalendarCellClassFunction<D> = (date: D, view: 'month' | 'year' | 'multi-year') => MatCalendarCellCssClasses;
151+
150152
export declare type MatCalendarCellCssClasses = string | string[] | Set<string> | {
151153
[key: string]: any;
152154
};
@@ -401,7 +403,7 @@ export declare class MatMonthView<D> implements AfterContentInit, OnDestroy {
401403
readonly activeDateChange: EventEmitter<D>;
402404
comparisonEnd: D | null;
403405
comparisonStart: D | null;
404-
dateClass: (date: D) => MatCalendarCellCssClasses;
406+
dateClass: MatCalendarCellClassFunction<D>;
405407
dateFilter: (date: D) => boolean;
406408
get maxDate(): D | null;
407409
set maxDate(value: D | null);
@@ -431,6 +433,7 @@ export declare class MatMultiYearView<D> implements AfterContentInit, OnDestroy
431433
get activeDate(): D;
432434
set activeDate(value: D);
433435
readonly activeDateChange: EventEmitter<D>;
436+
dateClass: MatCalendarCellClassFunction<D>;
434437
dateFilter: (date: D) => boolean;
435438
get maxDate(): D | null;
436439
set maxDate(value: D | null);
@@ -448,7 +451,7 @@ export declare class MatMultiYearView<D> implements AfterContentInit, OnDestroy
448451
_yearSelected(event: MatCalendarUserEvent<number>): void;
449452
ngAfterContentInit(): void;
450453
ngOnDestroy(): void;
451-
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatMultiYearView<any>, "mat-multi-year-view", ["matMultiYearView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; }, { "selectedChange": "selectedChange"; "yearSelected": "yearSelected"; "activeDateChange": "activeDateChange"; }, never, never>;
454+
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatMultiYearView<any>, "mat-multi-year-view", ["matMultiYearView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; }, { "selectedChange": "selectedChange"; "yearSelected": "yearSelected"; "activeDateChange": "activeDateChange"; }, never, never>;
452455
static ɵfac: i0.ɵɵFactoryDef<MatMultiYearView<any>, [null, { optional: true; }, { optional: true; }]>;
453456
}
454457

@@ -493,6 +496,7 @@ export declare class MatYearView<D> implements AfterContentInit, OnDestroy {
493496
get activeDate(): D;
494497
set activeDate(value: D);
495498
readonly activeDateChange: EventEmitter<D>;
499+
dateClass: MatCalendarCellClassFunction<D>;
496500
dateFilter: (date: D) => boolean;
497501
get maxDate(): D | null;
498502
set maxDate(value: D | null);
@@ -509,7 +513,7 @@ export declare class MatYearView<D> implements AfterContentInit, OnDestroy {
509513
_monthSelected(event: MatCalendarUserEvent<number>): void;
510514
ngAfterContentInit(): void;
511515
ngOnDestroy(): void;
512-
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatYearView<any>, "mat-year-view", ["matYearView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; }, { "selectedChange": "selectedChange"; "monthSelected": "monthSelected"; "activeDateChange": "activeDateChange"; }, never, never>;
516+
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatYearView<any>, "mat-year-view", ["matYearView"], { "activeDate": "activeDate"; "selected": "selected"; "minDate": "minDate"; "maxDate": "maxDate"; "dateFilter": "dateFilter"; "dateClass": "dateClass"; }, { "selectedChange": "selectedChange"; "monthSelected": "monthSelected"; "activeDateChange": "activeDateChange"; }, never, never>;
513517
static ɵfac: i0.ɵɵFactoryDef<MatYearView<any>, [null, { optional: true; }, { optional: true; }, { optional: true; }]>;
514518
}
515519

0 commit comments

Comments
 (0)