Skip to content

Commit

Permalink
feat(datepicker): allow custom classes to be applied to individual da…
Browse files Browse the repository at this point in the history
…tes (#13971)

Adds the `dateClass` function which allows consumers to apply custom CSS classes to specific dates. This is useful for highlighting specific dates like a holiday.

Fixes #13943.
  • Loading branch information
crisbeto authored and vivian-hu-zz committed Nov 7, 2018
1 parent 69ffd33 commit 4be1b06
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/lib/datepicker/calendar-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<td *ngFor="let item of row; let colIndex = index"
role="gridcell"
class="mat-calendar-body-cell"
[ngClass]="item.cssClasses"
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
[class.mat-calendar-body-disabled]="!item.enabled"
[class.mat-calendar-body-active]="_isActiveCell(rowIndex, colIndex)"
Expand Down
18 changes: 14 additions & 4 deletions src/lib/datepicker/calendar-body.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
import {MatCalendarBody, MatCalendarCell, MatCalendarCellCssClasses} from './calendar-body';
import {By} from '@angular/platform-browser';


Expand Down Expand Up @@ -98,6 +98,14 @@ describe('MatCalendarBody', () => {
expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
expect(cellEls[10].classList).toContain('mat-calendar-body-active');
});

it('should set a class on even dates', () => {
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('1');
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2');
expect(cellEls[0].classList).not.toContain('even');
expect(cellEls[1].classList).toContain('even');
});

});

});
Expand All @@ -117,7 +125,9 @@ describe('MatCalendarBody', () => {
})
class StandardCalendarBody {
label = 'Jan 2017';
rows = [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14]].map(r => r.map(createCell));
rows = [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14]].map(row => {
return row.map(cell => createCell(cell, cell % 2 === 0 ? 'even' : undefined));
});
todayValue = 3;
selectedValue = 4;
labelMinRequiredCells = 3;
Expand All @@ -128,6 +138,6 @@ class StandardCalendarBody {
}
}

function createCell(value: number) {
return new MatCalendarCell(value, `${value}`, `${value}-label`, true);
function createCell(value: number, cellClasses?: MatCalendarCellCssClasses) {
return new MatCalendarCell(value, `${value}`, `${value}-label`, true, cellClasses);
}
8 changes: 7 additions & 1 deletion src/lib/datepicker/calendar-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import {
} from '@angular/core';
import {take} from 'rxjs/operators';

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

/**
* An internal class that represents the data corresponding to a single calendar cell.
* @docs-private
Expand All @@ -26,7 +31,8 @@ export class MatCalendarCell {
constructor(public value: number,
public displayValue: string,
public ariaLabel: string,
public enabled: boolean) {}
public enabled: boolean,
public cssClasses?: MatCalendarCellCssClasses) {}
}


Expand Down
1 change: 1 addition & 0 deletions src/lib/datepicker/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[dateFilter]="dateFilter"
[maxDate]="maxDate"
[minDate]="minDate"
[dateClass]="dateClass"
(selectedChange)="_dateSelected($event)"
(_userSelection)="_userSelected()">
</mat-month-view>
Expand Down
6 changes: 5 additions & 1 deletion src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {MatDatepickerIntl} from './datepicker-intl';
import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
import {MatCalendarCellCssClasses} from './calendar-body';

/**
* Possible views for the calendar.
Expand Down Expand Up @@ -220,9 +221,12 @@ export class MatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDes
}
private _maxDate: D | null;

/** A function used to filter which dates are selectable. */
/** Function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;

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

/** Emits when the currently selected date changes. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();

Expand Down
1 change: 1 addition & 0 deletions src/lib/datepicker/datepicker-content.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[dateFilter]="datepicker._dateFilter"
[headerComponent]="datepicker.calendarHeaderComponent"
[selected]="datepicker._selected"
[dateClass]="datepicker.dateClass"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker.select($event)"
(yearSelected)="datepicker._selectYear($event)"
Expand Down
8 changes: 8 additions & 0 deletions src/lib/datepicker/datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ application root module.
export class MyApp {}
```

#### Highlighting specific dates
If you want to apply one or more CSS classes to some dates in the calendar (e.g. to highlight a
holiday), you can do so with the `dateClass` input. It accepts a function which will be called
with each of the dates in the calendar and will apply any classes that are returned. The return
value can be anything that is accepted by `ngClass`.

<!-- example(datepicker-date-class) -->

### Accessibility

The `MatDatepickerInput` and `MatDatepickerToggle` directives add the `aria-haspopup` attribute to
Expand Down
4 changes: 4 additions & 0 deletions src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {MatCalendar} from './calendar';
import {matDatepickerAnimations} from './datepicker-animations';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerInput} from './datepicker-input';
import {MatCalendarCellCssClasses} from './calendar-body';

/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
Expand Down Expand Up @@ -212,6 +213,9 @@ export class MatDatepicker<D> implements OnDestroy, CanColor {
/** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */
@Input() panelClass: string | string[];

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

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

Expand Down
31 changes: 31 additions & 0 deletions src/lib/datepicker/month-view.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('MatMonthView', () => {
// Test components.
StandardMonthView,
MonthViewWithDateFilter,
MonthViewWithDateClass,
],
providers: [
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
Expand Down Expand Up @@ -291,6 +292,26 @@ describe('MatMonthView', () => {
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
});
});

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

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

let monthViewDebugElement = fixture.debugElement.query(By.directive(MatMonthView));
monthViewNativeElement = monthViewDebugElement.nativeElement;
});

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

});


Expand All @@ -312,3 +333,13 @@ class MonthViewWithDateFilter {
return date.getDate() % 2 == 0;
}
}

@Component({
template: `<mat-month-view [activeDate]="activeDate" [dateClass]="dateClass"></mat-month-view>`
})
class MonthViewWithDateClass {
activeDate = new Date(2017, JAN, 1);
dateClass(date: Date) {
return date.getDate() % 2 == 0 ? 'even' : undefined;
}
}
11 changes: 8 additions & 3 deletions src/lib/datepicker/month-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
import {Directionality} from '@angular/cdk/bidi';
import {MatCalendarBody, MatCalendarCell} from './calendar-body';
import {MatCalendarBody, MatCalendarCell, MatCalendarCellCssClasses} from './calendar-body';
import {createMissingDateImplError} from './datepicker-errors';


Expand Down Expand Up @@ -94,9 +94,12 @@ export class MatMonthView<D> implements AfterContentInit {
}
private _maxDate: D | null;

/** A function used to filter which dates are selectable. */
/** Function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;

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

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

Expand Down Expand Up @@ -273,8 +276,10 @@ export class MatMonthView<D> implements AfterContentInit {
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;

this._weeks[this._weeks.length - 1]
.push(new MatCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
.push(new MatCalendarCell(i + 1, dateNames[i], ariaLabel, enabled, cellClasses));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.example-custom-date-class {
background: orange;
border-radius: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<mat-form-field class="example-full-width">
<input matInput [matDatepicker]="picker" placeholder="Choose a date">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker [dateClass]="dateClass" #picker></mat-datepicker>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Component, ViewEncapsulation} from '@angular/core';

/** @title Datepicker with custom date classes */
@Component({
selector: 'datepicker-date-class-example',
templateUrl: 'datepicker-date-class-example.html',
styleUrls: ['datepicker-date-class-example.css'],
encapsulation: ViewEncapsulation.None,
})
export class DatepickerDateClassExample {
dateClass = (d: Date) => {
const date = d.getDate();

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

0 comments on commit 4be1b06

Please sign in to comment.