Skip to content

Commit 4e1bb26

Browse files
tobiasschweizermmalerba
authored andcommitted
feat(datepicker): Add Custom Header to DatePicker (#9639)
* feature (add custom header to mat-calendar) TODO: the custom header should be passed from datepicker * refactor (define custom header component) * refactor (remove useless text) * refactor (pass custom header component type as input) * refactor (add header as component portal) * refactor (custom header as component portal) - TODO: solve problem with directive cdkPortalOutlet in calendar.html * refactor (switch for custom header component) * refactor (define entry components) * fix (correct entry point) * refactor (custom header component): add test function to custom header component * refactor (some code formatting) * refactor (set indentation to two spaces) * refactor (formatting) * refactor (custom header for mat-calendar): renaming and code improvements * refactor (calendarHeaderComponent) rename input of MatDatepicker * refactor (calendarHeaderComponent): make a demo for custom header * refactor (calendar header): make calendar header portal variable public * refactor (calendar header): formatting * tests (MatCalendar): fix tests * tests (calendar): fix import issue * tests (MatCalendar): remove unused import * tests (MatCalendar): import MatDatepickerModule without redeclaration * tests (MatCalendar): fix imports * refactor (MatCalendar): default header comes with empty template * refactor (MatCalendar): remove useless newline * feature (MatCalendarHeader): move header in own template * refactor (MatCalendar): some code reformatting * fix (MatCalendar): add missin change detection for MatCalendarHeader * refactor (MatCalendar): fix indentation * fix (MatCalendar): clean stream up after component destruction * fix (MatCalendar): fix change detection for MatCalendarHeader * refactor (MatCalendarHeader): add dateadapter to constructor * refactor (MatCalendarHeader): move methods to calendar header (ongoing) * refactor (MatCalendarHeader): move label getter methods to MatCalendarHeader * refactor (MatCalendarHeader): move MatCalendarHeader component in a separate file * tests (MatCalendarHeader): move header related tests from MatCalendar (ongoing) * refactor (MatCalendar): linting * refactor (MatCalendarHeader): move MatCalendarHeader component back to calendar.ts (to avoid circular deps) * refactor (MatCalendarHeader): move header related methods * feature (MatCalendarHeader): add sample custom header controls to demo page * refactor (MatCalendarHeader): fix some styling issues * refactor (MatCalendarHeader): fix indentation * refactor (MatCalendarHeader): make calendar private for custom header * refactor (MatCalendarHeader): use type parameter for header (generics) * refactor (MatCalendarHeader): make template file for custom header on demo page * refactor (MatCalendarHeader): separate section own demo page * refactor (MatCalendarHeader): style custom header * refactor (MatCalendarHeader): fix styling issue * refactor (package-lock.json): revert changes to version on master branch * refactor (package-lock.json): revert to commit 6f63fd2 * fix (MatCalendarHeader): correct forward reference to MatCalendar * Add `stateChanges` to `MatCalendar` * fix nits
1 parent f7b5d34 commit 4e1bb26

14 files changed

+464
-263
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div class="custom-header">
2+
<button mat-icon-button (click)="previousClicked('year')">&lt;&lt;</button>
3+
<button mat-icon-button (click)="previousClicked('month')">&lt;</button>
4+
<span class="custom-header-label">{{periodLabel}}</span>
5+
<button mat-icon-button (click)="nextClicked('month')">&gt;</button>
6+
<button mat-icon-button (click)="nextClicked('year')">&gt;&gt;</button>
7+
</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.custom-header {
2+
padding: 1em 1.5em;
3+
display: flex;
4+
align-items: center;
5+
}
6+
7+
.custom-header-label {
8+
flex: 1;
9+
text-align: center;
10+
}

src/demo-app/datepicker/datepicker-demo.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,14 @@ <h2>Datepicker with value property binding</h2>
144144
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
145145
</mat-form-field>
146146
</p>
147+
148+
<h2>Datepicker with custom header</h2>
149+
<p>
150+
<mat-form-field>
151+
<mat-label>Custom calendar header</mat-label>
152+
<input matInput [matDatepicker]="customCalendarHeaderPicker"
153+
[disabled]="inputDisabled">
154+
<mat-datepicker-toggle matSuffix [for]="customCalendarHeaderPicker"></mat-datepicker-toggle>
155+
<mat-datepicker #customCalendarHeaderPicker [touchUi]="touch" [disabled]="datepickerDisabled" [calendarHeaderComponent]="customHeader"></mat-datepicker>
156+
</mat-form-field>
157+
</p>

src/demo-app/datepicker/datepicker-demo.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ChangeDetectionStrategy, Component} from '@angular/core';
9+
import {ChangeDetectionStrategy, Component, Host} from '@angular/core';
1010
import {FormControl} from '@angular/forms';
1111
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
12+
import {DateAdapter} from '@angular/material/core';
13+
import {MatCalendar} from '@angular/material';
1214
import {ThemePalette} from '@angular/material/core';
1315

14-
1516
@Component({
1617
moduleId: module.id,
1718
selector: 'datepicker-demo',
@@ -40,4 +41,38 @@ export class DatepickerDemo {
4041

4142
onDateInput = (e: MatDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
4243
onDateChange = (e: MatDatepickerInputEvent<Date>) => this.lastDateChange = e.value;
44+
45+
// pass custom header component type as input
46+
customHeader = CustomHeader;
47+
}
48+
49+
// Custom header component for datepicker
50+
@Component({
51+
moduleId: module.id,
52+
selector: 'custom-header',
53+
templateUrl: 'custom-header.html',
54+
styleUrls: ['custom-header.css'],
55+
changeDetection: ChangeDetectionStrategy.OnPush,
56+
})
57+
export class CustomHeader<D> {
58+
constructor(@Host() private _calendar: MatCalendar<D>,
59+
private _dateAdapter: DateAdapter<D>) {}
60+
61+
get periodLabel() {
62+
const year = this._dateAdapter.getYearName(this._calendar.activeDate);
63+
const month = (this._dateAdapter.getMonth(this._calendar.activeDate) + 1);
64+
return `${month}/${year}`;
65+
}
66+
67+
previousClicked(mode: 'month' | 'year') {
68+
this._calendar.activeDate = mode == 'month' ?
69+
this._dateAdapter.addCalendarMonths(this._calendar.activeDate, -1) :
70+
this._dateAdapter.addCalendarYears(this._calendar.activeDate, -1);
71+
}
72+
73+
nextClicked(mode: 'month' | 'year') {
74+
this._calendar.activeDate = mode == 'month' ?
75+
this._dateAdapter.addCalendarMonths(this._calendar.activeDate, 1) :
76+
this._dateAdapter.addCalendarYears(this._calendar.activeDate, 1);
77+
}
4378
}

src/demo-app/demo-app/demo-module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {ButtonDemo} from '../button/button-demo';
1919
import {CardDemo} from '../card/card-demo';
2020
import {CheckboxDemo, MatCheckboxDemoNestedChecklist} from '../checkbox/checkbox-demo';
2121
import {ChipsDemo} from '../chips/chips-demo';
22-
import {DatepickerDemo} from '../datepicker/datepicker-demo';
22+
import {CustomHeader, DatepickerDemo} from '../datepicker/datepicker-demo';
2323
import {DemoMaterialModule} from '../demo-material-module';
2424
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from '../dialog/dialog-demo';
2525
import {DrawerDemo} from '../drawer/drawer-demo';
@@ -88,6 +88,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
8888
ChipsDemo,
8989
ContentElementDialog,
9090
DatepickerDemo,
91+
CustomHeader,
9192
DemoApp,
9293
DialogDemo,
9394
DrawerDemo,
@@ -148,6 +149,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
148149
ScienceJoke,
149150
SpagettiPanel,
150151
ExampleBottomSheet,
152+
CustomHeader,
151153
DemoOverlay,
152154
],
153155
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<div class="mat-calendar-header">
2+
<div class="mat-calendar-controls">
3+
<button mat-button class="mat-calendar-period-button"
4+
(click)="currentPeriodClicked()" [attr.aria-label]="periodButtonLabel">
5+
{{periodButtonText}}
6+
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="calendar.currentView != 'month'"></div>
7+
</button>
8+
9+
<div class="mat-calendar-spacer"></div>
10+
11+
<button mat-icon-button class="mat-calendar-previous-button"
12+
[disabled]="!previousEnabled()" (click)="previousClicked()"
13+
[attr.aria-label]="prevButtonLabel">
14+
</button>
15+
16+
<button mat-icon-button class="mat-calendar-next-button"
17+
[disabled]="!nextEnabled()" (click)="nextClicked()"
18+
[attr.aria-label]="nextButtonLabel">
19+
</button>
20+
</div>
21+
</div>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import {Direction, Directionality} from '@angular/cdk/bidi';
2+
import {MatDatepickerModule} from './datepicker-module';
3+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
4+
import {MatDatepickerIntl} from './datepicker-intl';
5+
import {DEC, FEB, JAN, MatNativeDateModule} from '@angular/material/core';
6+
import {Component} from '@angular/core';
7+
import {MatCalendar} from './calendar';
8+
import {By} from '@angular/platform-browser';
9+
import {yearsPerPage} from './multi-year-view';
10+
11+
describe('MatCalendarHeader', () => {
12+
let dir: { value: Direction };
13+
14+
beforeEach(async(() => {
15+
TestBed.configureTestingModule({
16+
imports: [
17+
MatNativeDateModule,
18+
MatDatepickerModule,
19+
],
20+
declarations: [
21+
// Test components.
22+
StandardCalendar,
23+
],
24+
providers: [
25+
MatDatepickerIntl,
26+
{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
27+
],
28+
});
29+
30+
TestBed.compileComponents();
31+
}));
32+
33+
describe('standard calendar', () => {
34+
let fixture: ComponentFixture<StandardCalendar>;
35+
let testComponent: StandardCalendar;
36+
let calendarElement: HTMLElement;
37+
let periodButton: HTMLElement;
38+
let prevButton: HTMLElement;
39+
let nextButton: HTMLElement;
40+
let calendarInstance: MatCalendar<Date>;
41+
42+
beforeEach(() => {
43+
fixture = TestBed.createComponent(StandardCalendar);
44+
fixture.detectChanges();
45+
46+
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
47+
calendarElement = calendarDebugElement.nativeElement;
48+
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
49+
prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
50+
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
51+
52+
calendarInstance = calendarDebugElement.componentInstance;
53+
testComponent = fixture.componentInstance;
54+
});
55+
56+
it('should be in month view with specified month active', () => {
57+
expect(calendarInstance.currentView).toBe('month');
58+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
59+
});
60+
61+
it('should toggle view when period clicked', () => {
62+
expect(calendarInstance.currentView).toBe('month');
63+
64+
periodButton.click();
65+
fixture.detectChanges();
66+
67+
expect(calendarInstance.currentView).toBe('multi-year');
68+
69+
periodButton.click();
70+
fixture.detectChanges();
71+
72+
expect(calendarInstance.currentView).toBe('month');
73+
});
74+
75+
it('should go to next and previous month', () => {
76+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
77+
78+
nextButton.click();
79+
fixture.detectChanges();
80+
81+
expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
82+
83+
prevButton.click();
84+
fixture.detectChanges();
85+
86+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 28));
87+
});
88+
89+
it('should go to previous and next year', () => {
90+
periodButton.click();
91+
fixture.detectChanges();
92+
93+
expect(calendarInstance.currentView).toBe('multi-year');
94+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
95+
96+
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
97+
fixture.detectChanges();
98+
99+
expect(calendarInstance.currentView).toBe('year');
100+
101+
nextButton.click();
102+
fixture.detectChanges();
103+
104+
expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
105+
106+
prevButton.click();
107+
fixture.detectChanges();
108+
109+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
110+
});
111+
112+
it('should go to previous and next multi-year range', () => {
113+
periodButton.click();
114+
fixture.detectChanges();
115+
116+
expect(calendarInstance.currentView).toBe('multi-year');
117+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
118+
119+
nextButton.click();
120+
fixture.detectChanges();
121+
122+
expect(calendarInstance.activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
123+
124+
prevButton.click();
125+
fixture.detectChanges();
126+
127+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
128+
});
129+
130+
it('should go back to month view after selecting year and month', () => {
131+
periodButton.click();
132+
fixture.detectChanges();
133+
134+
expect(calendarInstance.currentView).toBe('multi-year');
135+
expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
136+
137+
let yearCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
138+
(yearCells[0] as HTMLElement).click();
139+
fixture.detectChanges();
140+
141+
expect(calendarInstance.currentView).toBe('year');
142+
expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 31));
143+
144+
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
145+
(monthCells[monthCells.length - 1] as HTMLElement).click();
146+
fixture.detectChanges();
147+
148+
expect(calendarInstance.currentView).toBe('month');
149+
expect(calendarInstance.activeDate).toEqual(new Date(2016, DEC, 31));
150+
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
151+
});
152+
153+
});
154+
});
155+
156+
@Component({
157+
template: `
158+
<mat-calendar
159+
[startAt]="startDate"
160+
[(selected)]="selected"
161+
(yearSelected)="selectedYear=$event"
162+
(monthSelected)="selectedMonth=$event">
163+
</mat-calendar>`
164+
})
165+
class StandardCalendar {
166+
selected: Date;
167+
selectedYear: Date;
168+
selectedMonth: Date;
169+
startDate = new Date(2017, JAN, 31);
170+
}

src/lib/datepicker/calendar.html

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,10 @@
1-
<div class="mat-calendar-header">
2-
<div class="mat-calendar-controls">
3-
<button mat-button class="mat-calendar-period-button"
4-
(click)="_currentPeriodClicked()" [attr.aria-label]="_periodButtonLabel">
5-
{{_periodButtonText}}
6-
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="_currentView != 'month'"></div>
7-
</button>
81

9-
<div class="mat-calendar-spacer"></div>
2+
<ng-template [cdkPortalOutlet]="_calendarHeaderPortal"></ng-template>
103

11-
<button mat-icon-button class="mat-calendar-previous-button"
12-
[disabled]="!_previousEnabled()" (click)="_previousClicked()"
13-
[attr.aria-label]="_prevButtonLabel">
14-
</button>
15-
16-
<button mat-icon-button class="mat-calendar-next-button"
17-
[disabled]="!_nextEnabled()" (click)="_nextClicked()"
18-
[attr.aria-label]="_nextButtonLabel">
19-
</button>
20-
</div>
21-
</div>
22-
23-
<div class="mat-calendar-content" [ngSwitch]="_currentView" cdkMonitorSubtreeFocus tabindex="-1">
4+
<div class="mat-calendar-content" [ngSwitch]="currentView" cdkMonitorSubtreeFocus tabindex="-1">
245
<mat-month-view
256
*ngSwitchCase="'month'"
26-
[(activeDate)]="_activeDate"
7+
[(activeDate)]="activeDate"
278
[selected]="selected"
289
[dateFilter]="dateFilter"
2910
[maxDate]="maxDate"
@@ -34,7 +15,7 @@
3415

3516
<mat-year-view
3617
*ngSwitchCase="'year'"
37-
[activeDate]="_activeDate"
18+
[activeDate]="activeDate"
3819
[selected]="selected"
3920
[dateFilter]="dateFilter"
4021
[maxDate]="maxDate"
@@ -45,7 +26,7 @@
4526

4627
<mat-multi-year-view
4728
*ngSwitchCase="'multi-year'"
48-
[activeDate]="_activeDate"
29+
[activeDate]="activeDate"
4930
[selected]="selected"
5031
[dateFilter]="dateFilter"
5132
[maxDate]="maxDate"

0 commit comments

Comments
 (0)