Skip to content

feat(datepicker): Add Custom Header to DatePicker #9639

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 64 commits into from
Mar 22, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6251e1e
feature (add custom header to mat-calendar)
Jan 26, 2018
8d30140
refactor (define custom header component)
Jan 26, 2018
81c89fa
refactor (remove useless text)
Jan 26, 2018
737a563
refactor (pass custom header component type as input)
Jan 27, 2018
998be7c
refactor (add header as component portal)
Jan 27, 2018
a14dbd4
refactor (custom header as component portal)
Jan 27, 2018
585407f
refactor (switch for custom header component)
Jan 27, 2018
6668ae6
refactor (define entry components)
Jan 27, 2018
f9628a8
fix (correct entry point)
Jan 27, 2018
050904b
refactor (custom header component): add test function to custom heade…
Jan 27, 2018
a2db283
refactor (some code formatting)
Jan 27, 2018
7f071ec
refactor (set indentation to two spaces)
Jan 29, 2018
5c9f662
refactor (formatting)
Jan 30, 2018
3467c0a
refactor (custom header for mat-calendar): renaming and code improvem…
Jan 31, 2018
014777a
refactor (calendarHeaderComponent) rename input of MatDatepicker
Jan 31, 2018
179c9af
refactor (calendarHeaderComponent): make a demo for custom header
Jan 31, 2018
b6938f2
refactor (calendar header): make calendar header portal variable public
Feb 1, 2018
0faefc4
refactor (calendar header): formatting
Feb 1, 2018
aa8f837
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Feb 1, 2018
2b4d4d2
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Feb 9, 2018
b238107
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Feb 15, 2018
46b2c9d
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Feb 26, 2018
a45f3ee
tests (MatCalendar): fix tests
Feb 26, 2018
f8c4c5b
tests (calendar): fix import issue
Feb 26, 2018
b6b50cc
tests (MatCalendar): remove unused import
Feb 26, 2018
ca7a0db
tests (MatCalendar): import MatDatepickerModule without redeclaration
Feb 26, 2018
2058afb
tests (MatCalendar): fix imports
Feb 26, 2018
f6f19e8
refactor (MatCalendar): default header comes with empty template
Feb 26, 2018
702317d
refactor (MatCalendar): remove useless newline
Feb 26, 2018
528361e
feature (MatCalendarHeader): move header in own template
Mar 3, 2018
e36dc71
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 7, 2018
a7975d2
refactor (MatCalendar): some code reformatting
Mar 8, 2018
02b5604
fix (MatCalendar): add missin change detection for MatCalendarHeader
Mar 8, 2018
57bd12c
refactor (MatCalendar): fix indentation
Mar 8, 2018
a88fecb
fix (MatCalendar): clean stream up after component destruction
Mar 8, 2018
4c66467
fix (MatCalendar): fix change detection for MatCalendarHeader
Mar 8, 2018
258929a
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 8, 2018
81147f1
refactor (MatCalendarHeader): add dateadapter to constructor
Mar 12, 2018
586d5da
refactor (MatCalendarHeader): move methods to calendar header (ongoing)
Mar 12, 2018
a9d4c58
refactor (MatCalendarHeader): move label getter methods to MatCalenda…
Mar 12, 2018
9bba08f
refactor (MatCalendarHeader): move MatCalendarHeader component in a s…
Mar 12, 2018
1ff7e45
tests (MatCalendarHeader): move header related tests from MatCalendar…
Mar 12, 2018
5196ee7
refactor (MatCalendar): linting
Mar 12, 2018
4cee00c
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 12, 2018
da49938
refactor (MatCalendarHeader): move MatCalendarHeader component back t…
Mar 12, 2018
70ff16f
refactor (MatCalendarHeader): move header related methods
Mar 12, 2018
533739e
feature (MatCalendarHeader): add sample custom header controls to dem…
Mar 12, 2018
c287495
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 15, 2018
6d0a4d2
refactor (MatCalendarHeader): fix some styling issues
Mar 16, 2018
a33b70c
refactor (MatCalendarHeader): fix indentation
Mar 16, 2018
c7f21bf
refactor (MatCalendarHeader): make calendar private for custom header
Mar 16, 2018
715e413
refactor (MatCalendarHeader): use type parameter for header (generics)
Mar 16, 2018
5f4b0fc
refactor (MatCalendarHeader): make template file for custom header on…
Mar 16, 2018
3057f9d
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 16, 2018
d81db60
refactor (MatCalendarHeader): separate section own demo page
Mar 16, 2018
94c825a
refactor (MatCalendarHeader): style custom header
Mar 16, 2018
b6fc003
refactor (MatCalendarHeader): fix styling issue
Mar 16, 2018
796cbf2
refactor (package-lock.json): revert changes to version on master branch
Mar 21, 2018
abf1c62
refactor (package-lock.json): revert to commit 6f63fd283eeb643523a9e3…
Mar 21, 2018
1fdb1c3
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 21, 2018
822dd56
fix (MatCalendarHeader): correct forward reference to MatCalendar
Mar 21, 2018
ba4e839
Merge branch 'master' into tobiasschweizer/calendar-header-comp
Mar 21, 2018
1ee0f93
Add `stateChanges` to `MatCalendar`
mmalerba Mar 21, 2018
aff641d
fix nits
mmalerba Mar 21, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,297 changes: 1,101 additions & 196 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ <h2>Options</h2>
<mat-datepicker #startAtPicker [touchUi]="touch" [disabled]="datepickerDisabled"></mat-datepicker>
</mat-form-field>
</p>
<p>
<mat-form-field>
<input matInput [matDatepicker]="customCalendarHeaderPicker" placeholder="Custom calendar header"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of placeholder="Custom calendar header" can you do <mat-label>Custom calendar header</mat-label>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 6d0a4d2

[disabled]="inputDisabled">
<mat-datepicker-toggle matSuffix [for]="customCalendarHeaderPicker"></mat-datepicker-toggle>
<mat-datepicker #customCalendarHeaderPicker [touchUi]="touch" [disabled]="datepickerDisabled" [calendarHeaderComponent]="customHeader"></mat-datepicker>
</mat-form-field>
</p>

<h2>Result</h2>

Expand Down
20 changes: 18 additions & 2 deletions src/demo-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* 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';


@Component({
Expand All @@ -32,10 +34,24 @@ export class DatepickerDemo {
lastDateChange: Date | null;

dateFilter =
(date: Date) => !(date.getFullYear() % 2) && (date.getMonth() % 2) && !(date.getDate() % 2)
(date: Date) => !(date.getFullYear() % 2) && (date.getMonth() % 2) && !(date.getDate() % 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: continuation lines indented 2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 0faefc4


onDateInput = (e: MatDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
onDateChange = (e: MatDatepickerInputEvent<Date>) => this.lastDateChange = e.value;

dateCtrl = new FormControl();

// pass custom header component type as input
customHeader = CustomHeader;
}

// Custom header component for datepicker
@Component({
selector: 'custom-header',
template: 'custom header'
})
export class CustomHeader {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using below, can you add a type parameter to this class CustomHeader<T>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 715e413

constructor(@Host() public calendar: MatCalendar<any>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well make calendar private too, since it doesn't need to be public

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c7f21bf

public adapter: DateAdapter<any>) {
}
}
4 changes: 3 additions & 1 deletion src/demo-app/demo-app/demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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';
Expand Down Expand Up @@ -79,6 +79,7 @@ import {TableDemoModule} from '../table/table-demo-module';
ChipsDemo,
ContentElementDialog,
DatepickerDemo,
CustomHeader,
DemoApp,
DialogDemo,
DrawerDemo,
Expand Down Expand Up @@ -133,6 +134,7 @@ import {TableDemoModule} from '../table/table-demo-module';
RotiniPanel,
ScienceJoke,
SpagettiPanel,
CustomHeader,
],
})
export class DemoModule {}
3 changes: 3 additions & 0 deletions src/lib/datepicker/calendar.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div class="mat-calendar-header">

<ng-template [cdkPortalOutlet]="calendarHeaderPortal"></ng-template>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning to move the .mat-calendar-controls below into the mat-calendar-header? That way we can allow people to swap out the default controls for a custom set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea.

In that case it would make sense to add them to mat-calendar-header (component MatCalendarHeader) which is the default.

So the template of MatCalendarHeader should contain:

<div class="mat-calendar-controls">
    <button mat-button class="mat-calendar-period-button"
            (click)="_currentPeriodClicked()"  [attr.aria-label]="_periodButtonLabel">
      {{_periodButtonText}}
      <div class="mat-calendar-arrow" [class.mat-calendar-invert]="_currentView != 'month'"></div>
    </button>

    <div class="mat-calendar-spacer"></div>

    <button mat-icon-button class="mat-calendar-previous-button"
            [disabled]="!_previousEnabled()" (click)="_previousClicked()"
            [attr.aria-label]="_prevButtonLabel">
    </button>

    <button mat-icon-button class="mat-calendar-next-button"
            [disabled]="!_nextEnabled()" (click)="_nextClicked()"
            [attr.aria-label]="_nextButtonLabel">
    </button>
  </div>

And then we could remove that code from calendar.html since it will be inserted by <ng-template [cdkPortalOutlet]="_calendarHeaderPortal"></ng-template>.

However, if someone provides a custom header, she would have to take care of these controls.

Could we make more sub-components for the buttons so they could be combined in a more modular way?

Could we make components for the buttons (previous, next)? But how would that work with the callback methods that have to be called on MatCalendar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I missed this comment somehow. Yeah, that's pretty much what I was thinking. We could package the buttons up into their own components, but it seems a little bit overkill to me since they're not very complex. We will need to make some of the internal properties and methods on calendar public (remove the _) to indicate that its ok for people to inject the calendar and call those methods in their custom header

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Mar 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmalerba done in 528361e

One test is failing in src/lib/datepicker/calendar.spec.ts:

it('should re-render when the i18n labels have changed',
      inject([MatDatepickerIntl], (intl: MatDatepickerIntl) => {
        const button = fixture.debugElement.nativeElement
            .querySelector('.mat-calendar-period-button');

        intl.switchToMultiYearViewLabel = 'Go to multi-year view?';
        intl.changes.next();
        fixture.detectChanges();

        expect(button.getAttribute('aria-label')).toBe('Go to multi-year view?');
      })
    );

And another test does not pass in src/lib/select/select.spec.ts:

it('should emit to `optionSelectionChanges` when an option is selected', fakeAsync(() => {
        trigger.click();
        fixture.detectChanges();
        flush();

        const spy = jasmine.createSpy('option selection spy');
        const subscription = fixture.componentInstance.select.optionSelectionChanges.subscribe(spy);
        const option = overlayContainerElement.querySelector('mat-option') as HTMLElement;
        option.click();
        fixture.detectChanges();
        flush();

        expect(spy).toHaveBeenCalledWith(jasmine.any(MatOptionSelectionChange));

        subscription.unsubscribe();
      }));

I assume that this related to the use of querySelector that tries to get an element that was moved into another template (at least in the first case). I do not know why the second test fails. I cannot find the element in the MatCalendarHeader template. But maybe it is the menu that pops up.


<div class="mat-calendar-controls">
<button mat-button class="mat-calendar-period-button"
(click)="_currentPeriodClicked()" [attr.aria-label]="_periodButtonLabel">
Expand Down
29 changes: 28 additions & 1 deletion src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,20 @@ import {MatMonthView} from './month-view';
import {MatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view';
import {MatYearView} from './year-view';
import {Directionality} from '@angular/cdk/bidi';
import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal';

/**
* Default header of a [MatCalendar].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** Default header for MatCalendar */

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 0faefc4

*/
@Component({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint: Missing required properties: encapsulation, moduleId, preserveWhitespaces, changeDetection (can set them all to the same things MatCalendar below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a45f3ee

selector: 'mat-calendar-header',
template: 'default header'
})
export class MatCalendarHeader {
constructor() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove this blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a88fecb

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no newline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 4c66467

}
}

/**
* A calendar that is used as part of the datepicker.
Expand All @@ -64,6 +77,17 @@ import {Directionality} from '@angular/cdk/bidi';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a88fecb

/** An input indicating the type of the header component, if set. */
@Input() headerComponent: ComponentType<any>;

/** A portal containing the header component type for this calendar. */
private _calendarHeaderPortal: Portal<any>;

get calendarHeaderPortal() {
return this._calendarHeaderPortal;
}

private _intlChanges: Subscription;

/** A date representing the period (month or year) to start the calendar in. */
Expand Down Expand Up @@ -125,7 +149,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
(!this.dateFilter || this.dateFilter(date)) &&
(!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) &&
(!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0);
}
};

/**
* The current active date. This determines which time period is shown and which date is
Expand Down Expand Up @@ -200,6 +224,9 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
}

ngAfterContentInit() {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a88fecb

this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || MatCalendarHeader);

this._activeDate = this.startAt || this._dateAdapter.today();
this._focusActiveCell();
this._currentView = this.startView;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/datepicker/datepicker-content.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
[dateFilter]="datepicker._dateFilter"
[selected]="datepicker._selected"
(selectedChange)="datepicker._select($event)"
(_userSelection)="datepicker.close()">
(_userSelection)="datepicker.close()"
[headerComponent]="datepicker.calendarHeaderComponent">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move above () bindings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a7975d2

</mat-calendar>
6 changes: 5 additions & 1 deletion src/lib/datepicker/datepicker-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatDialogModule} from '@angular/material/dialog';
import {MatIconModule} from '@angular/material/icon';
import {MatCalendar} from './calendar';
import {MatCalendarHeader, MatCalendar} from './calendar';
import {MatCalendarBody} from './calendar-body';
import {
MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER,
Expand All @@ -26,6 +26,7 @@ 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({
Expand All @@ -36,6 +37,7 @@ import {MatYearView} from './year-view';
MatIconModule,
OverlayModule,
A11yModule,
PortalModule,
],
exports: [
MatCalendar,
Expand All @@ -60,13 +62,15 @@ import {MatYearView} from './year-view';
MatMonthView,
MatYearView,
MatMultiYearView,
MatCalendarHeader
],
providers: [
MatDatepickerIntl,
MAT_DATEPICKER_SCROLL_STRATEGY_PROVIDER,
],
entryComponents: [
MatDatepickerContent,
MatCalendarHeader,
]
})
export class MatDatepickerModule {}
6 changes: 5 additions & 1 deletion src/lib/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
RepositionScrollStrategy,
ScrollStrategy,
} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
import {take} from 'rxjs/operators/take';
import {filter} from 'rxjs/operators/filter';
import {
Expand Down Expand Up @@ -115,6 +115,10 @@ export class MatDatepickerContent<D> implements AfterContentInit {
preserveWhitespaces: false,
})
export class MatDatepicker<D> implements OnDestroy {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no blank line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in a88fecb

/** An input indicating the type of the custom header component for the calendar, if set. */
@Input() calendarHeaderComponent: ComponentType<any>;

/** The date to open the calendar to initially. */
@Input()
get startAt(): D | null {
Expand Down