From 4e1bb265e361a79694388451cea2010b51fc88cc Mon Sep 17 00:00:00 2001
From: Tobias Schweizer
Date: Thu, 22 Mar 2018 17:59:29 +0100
Subject: [PATCH] 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 6f63fd283eeb643523a9e354d0e7ae6ad0563968
* fix (MatCalendarHeader): correct forward reference to MatCalendar
* Add `stateChanges` to `MatCalendar`
* fix nits
---
package-lock.json | 2 +-
src/demo-app/datepicker/custom-header.html | 7 +
src/demo-app/datepicker/custom-header.scss | 10 +
src/demo-app/datepicker/datepicker-demo.html | 11 +
src/demo-app/datepicker/datepicker-demo.ts | 39 ++-
src/demo-app/demo-app/demo-module.ts | 4 +-
src/lib/datepicker/calendar-header.html | 21 ++
src/lib/datepicker/calendar-header.spec.ts | 170 +++++++++++++
src/lib/datepicker/calendar.html | 29 +--
src/lib/datepicker/calendar.spec.ts | 169 +++----------
src/lib/datepicker/calendar.ts | 252 ++++++++++++-------
src/lib/datepicker/datepicker-content.html | 1 +
src/lib/datepicker/datepicker-module.ts | 7 +-
src/lib/datepicker/datepicker.ts | 5 +-
14 files changed, 464 insertions(+), 263 deletions(-)
create mode 100644 src/demo-app/datepicker/custom-header.html
create mode 100644 src/demo-app/datepicker/custom-header.scss
create mode 100644 src/lib/datepicker/calendar-header.html
create mode 100644 src/lib/datepicker/calendar-header.spec.ts
diff --git a/package-lock.json b/package-lock.json
index b6149e63f9c..a7bf587e303 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8136,7 +8136,7 @@
"github": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/github/-/github-12.1.0.tgz",
- "integrity": "sha512-HhWjhd/OATC4Hjj7xfGjGRtwWzo/fzTc55EkvsRatI9G6Vp47mVcdBIt1lQ56A9Qit/yVQRX1+M9jbWlcJvgug==",
+ "integrity": "sha1-8qLcvUQReBVZQiV0kaS8CL9mHdc=",
"dev": true,
"requires": {
"dotenv": "4.0.0",
diff --git a/src/demo-app/datepicker/custom-header.html b/src/demo-app/datepicker/custom-header.html
new file mode 100644
index 00000000000..2c3f30415c0
--- /dev/null
+++ b/src/demo-app/datepicker/custom-header.html
@@ -0,0 +1,7 @@
+
diff --git a/src/demo-app/datepicker/custom-header.scss b/src/demo-app/datepicker/custom-header.scss
new file mode 100644
index 00000000000..2d928abd5ed
--- /dev/null
+++ b/src/demo-app/datepicker/custom-header.scss
@@ -0,0 +1,10 @@
+.custom-header {
+ padding: 1em 1.5em;
+ display: flex;
+ align-items: center;
+}
+
+.custom-header-label {
+ flex: 1;
+ text-align: center;
+}
diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html
index e5771e8fa60..03af10a442b 100644
--- a/src/demo-app/datepicker/datepicker-demo.html
+++ b/src/demo-app/datepicker/datepicker-demo.html
@@ -144,3 +144,14 @@
[startView]="yearView ? 'year' : 'month'">
+
+Datepicker with custom header
+
+
+ Custom calendar header
+
+
+
+
+
diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts
index 61d6a514688..f01da839834 100644
--- a/src/demo-app/datepicker/datepicker-demo.ts
+++ b/src/demo-app/datepicker/datepicker-demo.ts
@@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ChangeDetectionStrategy, Component} from '@angular/core';
+import {ChangeDetectionStrategy, Component, Host} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
+import {DateAdapter} from '@angular/material/core';
+import {MatCalendar} from '@angular/material';
import {ThemePalette} from '@angular/material/core';
-
@Component({
moduleId: module.id,
selector: 'datepicker-demo',
@@ -40,4 +41,38 @@ export class DatepickerDemo {
onDateInput = (e: MatDatepickerInputEvent) => this.lastDateInput = e.value;
onDateChange = (e: MatDatepickerInputEvent) => this.lastDateChange = e.value;
+
+ // pass custom header component type as input
+ customHeader = CustomHeader;
+}
+
+// Custom header component for datepicker
+@Component({
+ moduleId: module.id,
+ selector: 'custom-header',
+ templateUrl: 'custom-header.html',
+ styleUrls: ['custom-header.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CustomHeader {
+ constructor(@Host() private _calendar: MatCalendar,
+ private _dateAdapter: DateAdapter) {}
+
+ get periodLabel() {
+ const year = this._dateAdapter.getYearName(this._calendar.activeDate);
+ const month = (this._dateAdapter.getMonth(this._calendar.activeDate) + 1);
+ return `${month}/${year}`;
+ }
+
+ previousClicked(mode: 'month' | 'year') {
+ this._calendar.activeDate = mode == 'month' ?
+ this._dateAdapter.addCalendarMonths(this._calendar.activeDate, -1) :
+ this._dateAdapter.addCalendarYears(this._calendar.activeDate, -1);
+ }
+
+ nextClicked(mode: 'month' | 'year') {
+ this._calendar.activeDate = mode == 'month' ?
+ this._dateAdapter.addCalendarMonths(this._calendar.activeDate, 1) :
+ this._dateAdapter.addCalendarYears(this._calendar.activeDate, 1);
+ }
}
diff --git a/src/demo-app/demo-app/demo-module.ts b/src/demo-app/demo-app/demo-module.ts
index 9d116439a02..919b4e1e262 100644
--- a/src/demo-app/demo-app/demo-module.ts
+++ b/src/demo-app/demo-app/demo-module.ts
@@ -19,7 +19,7 @@ import {ButtonDemo} from '../button/button-demo';
import {CardDemo} from '../card/card-demo';
import {CheckboxDemo, MatCheckboxDemoNestedChecklist} from '../checkbox/checkbox-demo';
import {ChipsDemo} from '../chips/chips-demo';
-import {DatepickerDemo} from '../datepicker/datepicker-demo';
+import {CustomHeader, DatepickerDemo} from '../datepicker/datepicker-demo';
import {DemoMaterialModule} from '../demo-material-module';
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from '../dialog/dialog-demo';
import {DrawerDemo} from '../drawer/drawer-demo';
@@ -88,6 +88,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
ChipsDemo,
ContentElementDialog,
DatepickerDemo,
+ CustomHeader,
DemoApp,
DialogDemo,
DrawerDemo,
@@ -148,6 +149,7 @@ import {ConnectedOverlayDemo, DemoOverlay} from '../connected-overlay/connected-
ScienceJoke,
SpagettiPanel,
ExampleBottomSheet,
+ CustomHeader,
DemoOverlay,
],
})
diff --git a/src/lib/datepicker/calendar-header.html b/src/lib/datepicker/calendar-header.html
new file mode 100644
index 00000000000..c11663b6227
--- /dev/null
+++ b/src/lib/datepicker/calendar-header.html
@@ -0,0 +1,21 @@
+
diff --git a/src/lib/datepicker/calendar-header.spec.ts b/src/lib/datepicker/calendar-header.spec.ts
new file mode 100644
index 00000000000..a143b10f586
--- /dev/null
+++ b/src/lib/datepicker/calendar-header.spec.ts
@@ -0,0 +1,170 @@
+import {Direction, Directionality} from '@angular/cdk/bidi';
+import {MatDatepickerModule} from './datepicker-module';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {MatDatepickerIntl} from './datepicker-intl';
+import {DEC, FEB, JAN, MatNativeDateModule} from '@angular/material/core';
+import {Component} from '@angular/core';
+import {MatCalendar} from './calendar';
+import {By} from '@angular/platform-browser';
+import {yearsPerPage} from './multi-year-view';
+
+describe('MatCalendarHeader', () => {
+ let dir: { value: Direction };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ MatNativeDateModule,
+ MatDatepickerModule,
+ ],
+ declarations: [
+ // Test components.
+ StandardCalendar,
+ ],
+ providers: [
+ MatDatepickerIntl,
+ {provide: Directionality, useFactory: () => dir = {value: 'ltr'}}
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('standard calendar', () => {
+ let fixture: ComponentFixture;
+ let testComponent: StandardCalendar;
+ let calendarElement: HTMLElement;
+ let periodButton: HTMLElement;
+ let prevButton: HTMLElement;
+ let nextButton: HTMLElement;
+ let calendarInstance: MatCalendar;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StandardCalendar);
+ fixture.detectChanges();
+
+ let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
+ calendarElement = calendarDebugElement.nativeElement;
+ periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
+ prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
+ nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
+
+ calendarInstance = calendarDebugElement.componentInstance;
+ testComponent = fixture.componentInstance;
+ });
+
+ it('should be in month view with specified month active', () => {
+ expect(calendarInstance.currentView).toBe('month');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+ });
+
+ it('should toggle view when period clicked', () => {
+ expect(calendarInstance.currentView).toBe('month');
+
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('multi-year');
+
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('month');
+ });
+
+ it('should go to next and previous month', () => {
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+
+ nextButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
+
+ prevButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 28));
+ });
+
+ it('should go to previous and next year', () => {
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('multi-year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+
+ (calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('year');
+
+ nextButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
+
+ prevButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+ });
+
+ it('should go to previous and next multi-year range', () => {
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('multi-year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+
+ nextButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
+
+ prevButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+ });
+
+ it('should go back to month view after selecting year and month', () => {
+ periodButton.click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('multi-year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
+
+ let yearCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
+ (yearCells[0] as HTMLElement).click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 31));
+
+ let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
+ (monthCells[monthCells.length - 1] as HTMLElement).click();
+ fixture.detectChanges();
+
+ expect(calendarInstance.currentView).toBe('month');
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, DEC, 31));
+ expect(testComponent.selected).toBeFalsy('no date should be selected yet');
+ });
+
+ });
+});
+
+@Component({
+ template: `
+
+ `
+})
+class StandardCalendar {
+ selected: Date;
+ selectedYear: Date;
+ selectedMonth: Date;
+ startDate = new Date(2017, JAN, 31);
+}
diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html
index 1245f7a2f26..284affeb69f 100644
--- a/src/lib/datepicker/calendar.html
+++ b/src/lib/datepicker/calendar.html
@@ -1,29 +1,10 @@
-
-
-
+
{
let dir: {value: Direction};
@@ -23,16 +18,10 @@ describe('MatCalendar', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
- MatButtonModule,
MatNativeDateModule,
+ MatDatepickerModule,
],
declarations: [
- MatCalendar,
- MatCalendarBody,
- MatMonthView,
- MatYearView,
- MatMultiYearView,
-
// Test components.
StandardCalendar,
CalendarWithMinMax,
@@ -52,8 +41,6 @@ describe('MatCalendar', () => {
let testComponent: StandardCalendar;
let calendarElement: HTMLElement;
let periodButton: HTMLElement;
- let prevButton: HTMLElement;
- let nextButton: HTMLElement;
let calendarInstance: MatCalendar;
beforeEach(() => {
@@ -63,108 +50,14 @@ describe('MatCalendar', () => {
let calendarDebugElement = fixture.debugElement.query(By.directive(MatCalendar));
calendarElement = calendarDebugElement.nativeElement;
periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
- prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement;
- nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should be in month view with specified month active', () => {
- expect(calendarInstance._currentView).toBe('month');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
- });
-
- it('should toggle view when period clicked', () => {
- expect(calendarInstance._currentView).toBe('month');
-
- periodButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('multi-year');
-
- periodButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('month');
- });
-
- it('should go to next and previous month', () => {
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
-
- nextButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
-
- prevButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 28));
- });
-
- it('should go to previous and next year', () => {
- periodButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('multi-year');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
-
- (calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('year');
-
- nextButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
-
- prevButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
- });
-
- it('should go to previous and next multi-year range', () => {
- periodButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('multi-year');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
-
- nextButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2017 + yearsPerPage, JAN, 31));
-
- prevButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
- });
-
- it('should go back to month view after selecting year and month', () => {
- periodButton.click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('multi-year');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
-
- let yearCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
- (yearCells[0] as HTMLElement).click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('year');
- expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 31));
-
- let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
- (monthCells[monthCells.length - 1] as HTMLElement).click();
- fixture.detectChanges();
-
- expect(calendarInstance._currentView).toBe('month');
- expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
- expect(testComponent.selected).toBeFalsy('no date should be selected yet');
+ expect(calendarInstance.currentView).toBe('month');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should select date in month view', () => {
@@ -172,7 +65,7 @@ describe('MatCalendar', () => {
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('month');
+ expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
});
@@ -180,14 +73,14 @@ describe('MatCalendar', () => {
periodButton.click();
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('multi-year');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
+ expect(calendarInstance.currentView).toBe('multi-year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('year');
+ expect(calendarInstance.currentView).toBe('year');
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
@@ -199,8 +92,8 @@ describe('MatCalendar', () => {
periodButton.click();
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('multi-year');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
+ expect(calendarInstance.currentView).toBe('multi-year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
@@ -237,7 +130,7 @@ describe('MatCalendar', () => {
});
it('should initially set start date active', () => {
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should make the calendar body focusable', () => {
@@ -249,12 +142,12 @@ describe('MatCalendar', () => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('multi-year');
+ expect(calendarInstance.currentView).toBe('multi-year');
(calendarBodyEl.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('year');
+ expect(calendarInstance.currentView).toBe('year');
});
it('should return to month view on enter', () => {
@@ -266,8 +159,8 @@ describe('MatCalendar', () => {
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('month');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
+ expect(calendarInstance.currentView).toBe('month');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, FEB, 28));
expect(testComponent.selected).toBeUndefined();
});
});
@@ -277,7 +170,7 @@ describe('MatCalendar', () => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('multi-year');
+ expect(calendarInstance.currentView).toBe('multi-year');
});
it('should go to year view on enter', () => {
@@ -289,8 +182,8 @@ describe('MatCalendar', () => {
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('year');
- expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
+ expect(calendarInstance.currentView).toBe('year');
+ expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 31));
expect(testComponent.selected).toBeUndefined();
});
});
@@ -319,14 +212,14 @@ describe('MatCalendar', () => {
testComponent.startAt = new Date(2000, JAN, 1);
fixture.detectChanges();
- expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should clamp startAt value above max date', () => {
testComponent.startAt = new Date(2020, JAN, 1);
fixture.detectChanges();
- expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should not go back past min date', () => {
@@ -337,18 +230,18 @@ describe('MatCalendar', () => {
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
expect(prevButton.disabled).toBe(false, 'previous button should not be disabled');
- expect(calendarInstance._activeDate).toEqual(new Date(2016, FEB, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, FEB, 1));
prevButton.click();
fixture.detectChanges();
expect(prevButton.disabled).toBe(true, 'previous button should be disabled');
- expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
prevButton.click();
fixture.detectChanges();
- expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should not go forward past max date', () => {
@@ -359,18 +252,18 @@ describe('MatCalendar', () => {
calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
expect(nextButton.disabled).toBe(false, 'next button should not be disabled');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, DEC, 1));
nextButton.click();
fixture.detectChanges();
expect(nextButton.disabled).toBe(true, 'next button should be disabled');
- expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
nextButton.click();
fixture.detectChanges();
- expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
+ expect(calendarInstance.activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should re-render the month view when the minDate changes', () => {
@@ -501,8 +394,8 @@ describe('MatCalendar', () => {
});
it('should not allow selection of disabled date in month view', () => {
- expect(calendarInstance._currentView).toBe('month');
- expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
+ expect(calendarInstance.currentView).toBe('month');
+ expect(calendarInstance.activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
@@ -519,16 +412,16 @@ describe('MatCalendar', () => {
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
- calendarInstance._activeDate = new Date(2017, NOV, 1);
+ calendarInstance.activeDate = new Date(2017, NOV, 1);
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('year');
+ expect(calendarInstance.currentView).toBe('year');
tableBodyEl = calendarElement.querySelector('.mat-calendar-body') as HTMLElement;
dispatchKeyboardEvent(tableBodyEl, 'keydown', ENTER);
fixture.detectChanges();
- expect(calendarInstance._currentView).toBe('month');
+ expect(calendarInstance.currentView).toBe('month');
expect(testComponent.selected).toBeUndefined();
});
});
diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts
index 1d25bd2b86b..966718f193e 100644
--- a/src/lib/datepicker/calendar.ts
+++ b/src/lib/datepicker/calendar.ts
@@ -6,12 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
+ forwardRef,
+ Host,
Inject,
Input,
OnChanges,
@@ -23,6 +26,9 @@ import {
ViewEncapsulation,
} from '@angular/core';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats} from '@angular/material/core';
+import {Observable} from 'rxjs/Observable';
+import {takeUntil} from 'rxjs/operators/takeUntil';
+import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {createMissingDateImplError} from './datepicker-errors';
import {MatDatepickerIntl} from './datepicker-intl';
@@ -30,6 +36,127 @@ import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage} from './multi-year-view';
import {MatYearView} from './year-view';
+/** Default header for MatCalendar */
+@Component({
+ moduleId: module.id,
+ selector: 'mat-calendar-header',
+ templateUrl: 'calendar-header.html',
+ encapsulation: ViewEncapsulation.None,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MatCalendarHeader implements OnDestroy {
+ /** Subject that emits when the component has been destroyed. */
+ private _destroyed = new Subject();
+
+ constructor(private _intl: MatDatepickerIntl,
+ @Host() @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar,
+ @Optional() private _dateAdapter: DateAdapter,
+ @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
+ changeDetectorRef: ChangeDetectorRef) {
+ this.calendar.stateChanges.pipe(takeUntil(this._destroyed))
+ .subscribe(() => changeDetectorRef.markForCheck());
+ }
+
+ /** The label for the current calendar view. */
+ get periodButtonText(): string {
+ if (this.calendar.currentView == 'month') {
+ return this._dateAdapter
+ .format(this.calendar.activeDate, this._dateFormats.display.monthYearLabel)
+ .toLocaleUpperCase();
+ }
+ if (this.calendar.currentView == 'year') {
+ return this._dateAdapter.getYearName(this.calendar.activeDate);
+ }
+ const activeYear = this._dateAdapter.getYear(this.calendar.activeDate);
+ const firstYearInView = this._dateAdapter.getYearName(
+ this._dateAdapter.createDate(activeYear - activeYear % 24, 0, 1));
+ const lastYearInView = this._dateAdapter.getYearName(
+ this._dateAdapter.createDate(activeYear + yearsPerPage - 1 - activeYear % 24, 0, 1));
+ return `${firstYearInView} \u2013 ${lastYearInView}`;
+ }
+
+ get periodButtonLabel(): string {
+ return this.calendar.currentView == 'month' ?
+ this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel;
+ }
+
+ /** The label for the the previous button. */
+ get prevButtonLabel(): string {
+ return {
+ 'month': this._intl.prevMonthLabel,
+ 'year': this._intl.prevYearLabel,
+ 'multi-year': this._intl.prevMultiYearLabel
+ }[this.calendar.currentView];
+ }
+
+ /** The label for the the next button. */
+ get nextButtonLabel(): string {
+ return {
+ 'month': this._intl.nextMonthLabel,
+ 'year': this._intl.nextYearLabel,
+ 'multi-year': this._intl.nextMultiYearLabel
+ }[this.calendar.currentView];
+ }
+
+ /** Handles user clicks on the period label. */
+ currentPeriodClicked(): void {
+ this.calendar.currentView = this.calendar.currentView == 'month' ? 'multi-year' : 'month';
+ }
+
+ /** Handles user clicks on the previous button. */
+ previousClicked(): void {
+ this.calendar.activeDate = this.calendar.currentView == 'month' ?
+ this._dateAdapter.addCalendarMonths(this.calendar.activeDate, -1) :
+ this._dateAdapter.addCalendarYears(
+ this.calendar.activeDate, this.calendar.currentView == 'year' ? -1 : -yearsPerPage
+ );
+ }
+
+ /** Handles user clicks on the next button. */
+ nextClicked(): void {
+ this.calendar.activeDate = this.calendar.currentView == 'month' ?
+ this._dateAdapter.addCalendarMonths(this.calendar.activeDate, 1) :
+ this._dateAdapter.addCalendarYears(
+ this.calendar.activeDate,
+ this.calendar.currentView == 'year' ? 1 : yearsPerPage
+ );
+ }
+
+ /** Whether the previous period button is enabled. */
+ previousEnabled(): boolean {
+ if (!this.calendar.minDate) {
+ return true;
+ }
+ return !this.calendar.minDate ||
+ !this._isSameView(this.calendar.activeDate, this.calendar.minDate);
+ }
+
+ /** Whether the next period button is enabled. */
+ nextEnabled(): boolean {
+ return !this.calendar.maxDate ||
+ !this._isSameView(this.calendar.activeDate, this.calendar.maxDate);
+ }
+
+ /** Whether the two dates represent the same view in the current view mode (month or year). */
+ private _isSameView(date1: D, date2: D): boolean {
+ if (this.calendar.currentView == 'month') {
+ return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
+ this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2);
+ }
+ if (this.calendar.currentView == 'year') {
+ return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
+ }
+ // Otherwise we are in 'multi-year' view.
+ return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
+ Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
+ }
+
+ ngOnDestroy() {
+ this._destroyed.next();
+ this._destroyed.complete();
+ }
+}
/**
* A calendar that is used as part of the datepicker.
@@ -48,6 +175,12 @@ import {MatYearView} from './year-view';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges {
+ /** An input indicating the type of the header component, if set. */
+ @Input() headerComponent: ComponentType;
+
+ /** A portal containing the header component type for this calendar. */
+ _calendarHeaderPortal: Portal;
+
private _intlChanges: Subscription;
/** A date representing the period (month or year) to start the calendar in. */
@@ -119,56 +252,26 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges {
* The current active date. This determines which time period is shown and which date is
* highlighted when using keyboard navigation.
*/
- get _activeDate(): D { return this._clampedActiveDate; }
- set _activeDate(value: D) {
+ get activeDate(): D { return this._clampedActiveDate; }
+ set activeDate(value: D) {
this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
+ this._stateChanges.next();
}
private _clampedActiveDate: D;
/** Whether the calendar is in month view. */
- _currentView: 'month' | 'year' | 'multi-year';
+ currentView: 'month' | 'year' | 'multi-year';
- /** The label for the current calendar view. */
- get _periodButtonText(): string {
- if (this._currentView == 'month') {
- return this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
- .toLocaleUpperCase();
- }
- if (this._currentView == 'year') {
- return this._dateAdapter.getYearName(this._activeDate);
- }
- const activeYear = this._dateAdapter.getYear(this._activeDate);
- const firstYearInView = this._dateAdapter.getYearName(
- this._dateAdapter.createDate(activeYear - activeYear % 24, 0, 1));
- const lastYearInView = this._dateAdapter.getYearName(
- this._dateAdapter.createDate(activeYear + yearsPerPage - 1 - activeYear % 24, 0, 1));
- return `${firstYearInView} \u2013 ${lastYearInView}`;
+ /**
+ * An observable that emits whenever there is a state change that the header may need to respond
+ * to.
+ */
+ get stateChanges(): Observable {
+ return this._stateChanges.asObservable();
}
+ private _stateChanges = new Subject();
- get _periodButtonLabel(): string {
- return this._currentView == 'month' ?
- this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel;
- }
-
- /** The label for the the previous button. */
- get _prevButtonLabel(): string {
- return {
- 'month': this._intl.prevMonthLabel,
- 'year': this._intl.prevYearLabel,
- 'multi-year': this._intl.prevMultiYearLabel
- }[this._currentView];
- }
-
- /** The label for the the next button. */
- get _nextButtonLabel(): string {
- return {
- 'month': this._intl.nextMonthLabel,
- 'year': this._intl.nextYearLabel,
- 'multi-year': this._intl.nextMultiYearLabel
- }[this._currentView];
- }
-
- constructor(private _intl: MatDatepickerIntl,
+ constructor(_intl: MatDatepickerIntl,
@Optional() private _dateAdapter: DateAdapter,
@Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
changeDetectorRef: ChangeDetectorRef) {
@@ -181,12 +284,17 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges {
throw createMissingDateImplError('MAT_DATE_FORMATS');
}
- this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
+ this._intlChanges = _intl.changes.subscribe(() => {
+ changeDetectorRef.markForCheck();
+ this._stateChanges.next();
+ });
}
ngAfterContentInit() {
- this._activeDate = this.startAt || this._dateAdapter.today();
- this._currentView = this.startView;
+ this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);
+
+ this.activeDate = this.startAt || this._dateAdapter.today();
+ this.currentView = this.startView;
}
ngOnDestroy() {
@@ -203,6 +311,8 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges {
view._init();
}
}
+
+ this._stateChanges.next();
}
/** Handles date selection in the month view. */
@@ -228,56 +338,8 @@ export class MatCalendar implements AfterContentInit, OnDestroy, OnChanges {
/** Handles year/month selection in the multi-year/year views. */
_goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void {
- this._activeDate = date;
- this._currentView = view;
- }
-
- /** Handles user clicks on the period label. */
- _currentPeriodClicked(): void {
- this._currentView = this._currentView == 'month' ? 'multi-year' : 'month';
- }
-
- /** Handles user clicks on the previous button. */
- _previousClicked(): void {
- this._activeDate = this._currentView == 'month' ?
- this._dateAdapter.addCalendarMonths(this._activeDate, -1) :
- this._dateAdapter.addCalendarYears(
- this._activeDate, this._currentView == 'year' ? -1 : -yearsPerPage);
- }
-
- /** Handles user clicks on the next button. */
- _nextClicked(): void {
- this._activeDate = this._currentView == 'month' ?
- this._dateAdapter.addCalendarMonths(this._activeDate, 1) :
- this._dateAdapter.addCalendarYears(
- this._activeDate, this._currentView == 'year' ? 1 : yearsPerPage);
- }
-
- /** Whether the previous period button is enabled. */
- _previousEnabled(): boolean {
- if (!this.minDate) {
- return true;
- }
- return !this.minDate || !this._isSameView(this._activeDate, this.minDate);
- }
-
- /** Whether the next period button is enabled. */
- _nextEnabled(): boolean {
- return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
- }
-
- /** Whether the two dates represent the same view in the current view mode (month or year). */
- private _isSameView(date1: D, date2: D): boolean {
- if (this._currentView == 'month') {
- return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
- this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2);
- }
- if (this._currentView == 'year') {
- return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
- }
- // Otherwise we are in 'multi-year' view.
- return Math.floor(this._dateAdapter.getYear(date1) / yearsPerPage) ==
- Math.floor(this._dateAdapter.getYear(date2) / yearsPerPage);
+ this.activeDate = date;
+ this.currentView = view;
}
/**
diff --git a/src/lib/datepicker/datepicker-content.html b/src/lib/datepicker/datepicker-content.html
index 3ade43b7933..ea90dbc7108 100644
--- a/src/lib/datepicker/datepicker-content.html
+++ b/src/lib/datepicker/datepicker-content.html
@@ -6,6 +6,7 @@
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
+ [headerComponent]="datepicker.calendarHeaderComponent"
[selected]="datepicker._selected"
[@fadeInCalendar]="'enter'"
(selectedChange)="datepicker._select($event)"
diff --git a/src/lib/datepicker/datepicker-module.ts b/src/lib/datepicker/datepicker-module.ts
index 2e9460f7730..4efd6d18a4c 100644
--- a/src/lib/datepicker/datepicker-module.ts
+++ b/src/lib/datepicker/datepicker-module.ts
@@ -13,6 +13,7 @@ import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatDialogModule} from '@angular/material/dialog';
import {MatCalendar} from './calendar';
+import {MatCalendarHeader} from './calendar';
import {MatCalendarBody} from './calendar-body';
import {MatDatepicker, MatDatepickerContent} from './datepicker';
import {MatDatepickerInput} from './datepicker-input';
@@ -21,15 +22,17 @@ import {MatDatepickerToggle, MatDatepickerToggleIcon} from './datepicker-toggle'
import {MatMonthView} from './month-view';
import {MatMultiYearView} from './multi-year-view';
import {MatYearView} from './year-view';
+import {PortalModule} from '@angular/cdk/portal';
@NgModule({
imports: [
- A11yModule,
CommonModule,
MatButtonModule,
MatDialogModule,
OverlayModule,
+ A11yModule,
+ PortalModule,
],
exports: [
MatCalendar,
@@ -54,12 +57,14 @@ import {MatYearView} from './year-view';
MatMonthView,
MatYearView,
MatMultiYearView,
+ MatCalendarHeader
],
providers: [
MatDatepickerIntl,
],
entryComponents: [
MatDatepickerContent,
+ MatCalendarHeader,
]
})
export class MatDatepickerModule {}
diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts
index 0a5707b7bb7..036bb17701d 100644
--- a/src/lib/datepicker/datepicker.ts
+++ b/src/lib/datepicker/datepicker.ts
@@ -17,7 +17,7 @@ import {
PositionStrategy,
ScrollStrategy,
} from '@angular/cdk/overlay';
-import {ComponentPortal} from '@angular/cdk/portal';
+import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
@@ -176,6 +176,9 @@ export class MatDatepickerContent extends _MatDatepickerContentMixinBase
encapsulation: ViewEncapsulation.None,
})
export class MatDatepicker implements OnDestroy, CanColor {
+ /** An input indicating the type of the custom header component for the calendar, if set. */
+ @Input() calendarHeaderComponent: ComponentType;
+
/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {