Skip to content

Commit

Permalink
fix(datepicker): support dateClass on year and multi-year views
Browse files Browse the repository at this point in the history
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 angular#20017.
  • Loading branch information
crisbeto committed Aug 10, 2020
1 parent 25ce323 commit 0940758
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {MatCalendarCellCssClasses} from '@angular/material/datepicker';
import {MatCalendarCellClassFunction} from '@angular/material/datepicker';

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

// Highlight the 1st and 20th day of each month.
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
// Highlight the 1st and 20th day of each month.
return (date === 1 || date === 20) ? 'example-custom-date-class' : '';
}

return '';
}
}
8 changes: 5 additions & 3 deletions src/material/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import {
} from '@angular/core';
import {take} from 'rxjs/operators';

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

/** Function that can generate the extra classes that should be added to a calendar cell. */
export type MatCalendarCellClassFunction<D> =
(date: D, view: 'month' | 'year' | 'multi-year') => MatCalendarCellCssClasses;

/**
* An internal class that represents the data corresponding to a single calendar cell.
* @docs-private
Expand Down
2 changes: 2 additions & 0 deletions src/material/datepicker/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
[dateClass]="dateClass"
(monthSelected)="_monthSelectedInYearView($event)"
(selectedChange)="_goToDateInView($event, 'month')">
</mat-year-view>
Expand All @@ -32,6 +33,7 @@
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
[dateClass]="dateClass"
(yearSelected)="_yearSelectedInMultiYearView($event)"
(selectedChange)="_goToDateInView($event, 'year')">
</mat-multi-year-view>
Expand Down
4 changes: 2 additions & 2 deletions src/material/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
MatDateFormats,
} from '@angular/material/core';
import {Subject, Subscription} from 'rxjs';
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerIntl} from './datepicker-intl';
import {MatMonthView} from './month-view';
Expand Down Expand Up @@ -245,7 +245,7 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
@Input() dateFilter: (date: D) => boolean;

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

/** Start of the comparison range. */
@Input() comparisonStart: D | null;
Expand Down
4 changes: 2 additions & 2 deletions src/material/datepicker/datepicker-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {filter, take} from 'rxjs/operators';
import {MatCalendar} from './calendar';
import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {MatCalendarCellCssClasses, MatCalendarUserEvent} from './calendar-body';
import {MatCalendarUserEvent, MatCalendarCellClassFunction} from './calendar-body';
import {DateFilterFn} from './datepicker-input-base';
import {
ExtractDateTypeFromSelection,
Expand Down Expand Up @@ -322,7 +322,7 @@ export abstract class MatDatepickerBase<C extends MatDatepickerControl<D>, S,
@Input() panelClass: string | string[];

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

/** Emits when the datepicker has been opened. */
@Output('opened') openedStream: EventEmitter<void> = new EventEmitter<void>();
Expand Down
6 changes: 3 additions & 3 deletions src/material/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import {Directionality} from '@angular/cdk/bidi';
import {
MatCalendarBody,
MatCalendarCell,
MatCalendarCellCssClasses,
MatCalendarUserEvent,
MatCalendarCellClassFunction,
} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
import {Subscription} from 'rxjs';
Expand Down Expand Up @@ -120,7 +120,7 @@ export class MatMonthView<D> implements AfterContentInit, OnDestroy {
@Input() dateFilter: (date: D) => boolean;

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

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

this._weeks[this._weeks.length - 1].push(new MatCalendarCell<D>(i + 1, dateNames[i],
ariaLabel, enabled, cellClasses, this._getCellCompareValue(date)!, date));
Expand Down
34 changes: 34 additions & 0 deletions src/material/datepicker/multi-year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('MatMultiYearView', () => {
StandardMultiYearView,
MultiYearViewWithDateFilter,
MultiYearViewWithMinMaxDate,
MultiYearViewWithDateClass,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
Expand Down Expand Up @@ -343,6 +344,26 @@ describe('MatMultiYearView', () => {
expect(cells[9].classList).not.toContain('mat-calendar-body-disabled');
});
});

describe('month view with custom date classes', () => {
let fixture: ComponentFixture<MultiYearViewWithDateClass>;
let multiYearViewNativeElement: Element;

beforeEach(() => {
fixture = TestBed.createComponent(MultiYearViewWithDateClass);
fixture.detectChanges();

let multiYearViewDebugElement = fixture.debugElement.query(By.directive(MatMultiYearView))!;
multiYearViewNativeElement = multiYearViewDebugElement.nativeElement;
});

it('should be able to add a custom class to some dates', () => {
let cells = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect(cells[0].classList).toContain('even');
expect(cells[1].classList).not.toContain('even');
});
});

});

@Component({
Expand Down Expand Up @@ -387,3 +408,16 @@ class MultiYearViewWithMinMaxDate {
minDate: Date | null;
maxDate: Date | null;
}


@Component({
template: `
<mat-multi-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-multi-year-view>
`
})
class MultiYearViewWithDateClass {
activeDate = new Date(2017, JAN, 1);
dateClass(date: Date) {
return date.getFullYear() % 2 == 0 ? 'even' : undefined;
}
}
17 changes: 14 additions & 3 deletions src/material/datepicker/multi-year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import {
} from '@angular/core';
import {DateAdapter} from '@angular/material/core';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
import {
MatCalendarBody,
MatCalendarCell,
MatCalendarUserEvent,
MatCalendarCellClassFunction,
} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
import {Subscription} from 'rxjs';
import {startWith} from 'rxjs/operators';
Expand Down Expand Up @@ -109,6 +114,9 @@ export class MatMultiYearView<D> implements AfterContentInit, OnDestroy {
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;

/** Function that can be used to add custom CSS classes to date cells. */
@Input() dateClass: MatCalendarCellClassFunction<D>;

/** Emits when a new year is selected. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();

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

/** Creates an MatCalendarCell for the given year. */
private _createCellForYear(year: number) {
let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1));
return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year));
const date = this._dateAdapter.createDate(year, 0, 1);
const yearName = this._dateAdapter.getYearName(date);
const cellClasses = this.dateClass ? this.dateClass(date, 'year') : undefined;

return new MatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year), cellClasses);
}

/** Whether the given year is enabled. */
Expand Down
32 changes: 32 additions & 0 deletions src/material/datepicker/year-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('MatYearView', () => {
// Test components.
StandardYearView,
YearViewWithDateFilter,
YearViewWithDateClass,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
Expand Down Expand Up @@ -329,6 +330,26 @@ describe('MatYearView', () => {
});

});

describe('year view with custom date classes', () => {
let fixture: ComponentFixture<YearViewWithDateClass>;
let yearViewNativeElement: Element;

beforeEach(() => {
fixture = TestBed.createComponent(YearViewWithDateClass);
fixture.detectChanges();

let yearViewDebugElement = fixture.debugElement.query(By.directive(MatYearView))!;
yearViewNativeElement = yearViewDebugElement.nativeElement;
});

it('should be able to add a custom class to some month cells', () => {
let cells = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect(cells[0].classList).toContain('even');
expect(cells[1].classList).not.toContain('even');
});
});

});


Expand Down Expand Up @@ -368,3 +389,14 @@ class YearViewWithDateFilter {
return true;
}
}


@Component({
template: `<mat-year-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-year-view>`
})
class YearViewWithDateClass {
activeDate = new Date(2017, JAN, 1);
dateClass(date: Date) {
return date.getMonth() % 2 == 0 ? 'even' : undefined;
}
}
21 changes: 15 additions & 6 deletions src/material/datepicker/year-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ import {
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';
import {
MatCalendarBody,
MatCalendarCell,
MatCalendarUserEvent,
MatCalendarCellClassFunction,
} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';
import {Subscription} from 'rxjs';
import {startWith} from 'rxjs/operators';
Expand Down Expand Up @@ -103,6 +108,9 @@ export class MatYearView<D> implements AfterContentInit, OnDestroy {
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;

/** Function that can be used to add custom CSS classes to date cells. */
@Input() dateClass: MatCalendarCellClassFunction<D>;

/** Emits when a new month is selected. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();

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

/** Creates an MatCalendarCell for the given month. */
private _createCellForMonth(month: number, monthName: string) {
let ariaLabel = this._dateAdapter.format(
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
this._dateFormats.display.monthYearA11yLabel);
return new MatCalendarCell(
month, monthName.toLocaleUpperCase(), ariaLabel, this._shouldEnableMonth(month));
const date = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1);
const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.monthYearA11yLabel);
const cellClasses = this.dateClass ? this.dateClass(date, 'year') : undefined;

return new MatCalendarCell(month, monthName.toLocaleUpperCase(), ariaLabel,
this._shouldEnableMonth(month), cellClasses);
}

/** Whether the given month is enabled. */
Expand Down
12 changes: 8 additions & 4 deletions tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export declare class MatCalendar<D> implements AfterContentInit, AfterViewChecke
comparisonStart: D | null;
get currentView(): MatCalendarView;
set currentView(value: MatCalendarView);
dateClass: (date: D) => MatCalendarCellCssClasses;
dateClass: MatCalendarCellClassFunction<D>;
dateFilter: (date: D) => boolean;
headerComponent: ComponentType<any>;
get maxDate(): D | null;
Expand Down Expand Up @@ -147,6 +147,8 @@ export declare class MatCalendarCell<D = any> {
constructor(value: number, displayValue: string, ariaLabel: string, enabled: boolean, cssClasses?: MatCalendarCellCssClasses, compareValue?: number, rawValue?: D | undefined);
}

export declare type MatCalendarCellClassFunction<D> = (date: D, view: 'month' | 'year' | 'multi-year') => MatCalendarCellCssClasses;

export declare type MatCalendarCellCssClasses = string | string[] | Set<string> | {
[key: string]: any;
};
Expand Down Expand Up @@ -401,7 +403,7 @@ export declare class MatMonthView<D> implements AfterContentInit, OnDestroy {
readonly activeDateChange: EventEmitter<D>;
comparisonEnd: D | null;
comparisonStart: D | null;
dateClass: (date: D) => MatCalendarCellCssClasses;
dateClass: MatCalendarCellClassFunction<D>;
dateFilter: (date: D) => boolean;
get maxDate(): D | null;
set maxDate(value: D | null);
Expand Down Expand Up @@ -431,6 +433,7 @@ export declare class MatMultiYearView<D> implements AfterContentInit, OnDestroy
get activeDate(): D;
set activeDate(value: D);
readonly activeDateChange: EventEmitter<D>;
dateClass: MatCalendarCellClassFunction<D>;
dateFilter: (date: D) => boolean;
get maxDate(): D | null;
set maxDate(value: D | null);
Expand All @@ -448,7 +451,7 @@ export declare class MatMultiYearView<D> implements AfterContentInit, OnDestroy
_yearSelected(event: MatCalendarUserEvent<number>): void;
ngAfterContentInit(): void;
ngOnDestroy(): void;
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>;
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>;
static ɵfac: i0.ɵɵFactoryDef<MatMultiYearView<any>, [null, { optional: true; }, { optional: true; }]>;
}

Expand Down Expand Up @@ -493,6 +496,7 @@ export declare class MatYearView<D> implements AfterContentInit, OnDestroy {
get activeDate(): D;
set activeDate(value: D);
readonly activeDateChange: EventEmitter<D>;
dateClass: MatCalendarCellClassFunction<D>;
dateFilter: (date: D) => boolean;
get maxDate(): D | null;
set maxDate(value: D | null);
Expand All @@ -509,7 +513,7 @@ export declare class MatYearView<D> implements AfterContentInit, OnDestroy {
_monthSelected(event: MatCalendarUserEvent<number>): void;
ngAfterContentInit(): void;
ngOnDestroy(): void;
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>;
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>;
static ɵfac: i0.ɵɵFactoryDef<MatYearView<any>, [null, { optional: true; }, { optional: true; }, { optional: true; }]>;
}

Expand Down

0 comments on commit 0940758

Please sign in to comment.