-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
feat(datepicker): Add Custom Header to DatePicker #9639
feat(datepicker): Add Custom Header to DatePicker #9639
Conversation
TODO: the custom header should be passed from datepicker
- TODO: solve problem with directive cdkPortalOutlet in calendar.html
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
templateUrl: 'datepicker-demo.html', | ||
styleUrls: ['datepicker-demo.css'], | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
moduleId: module.id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your formatting should be 2 spaces instead of a tab or four characters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Chan4077 Thanks for pointing that out. I changed the indentation to two spaces, undoing the changes introduced previously thereby.
@tobiasschweizer Could you sign the CLA? |
I did on behalf of my organization (https://github.com/dhlab-basel). Probably my boss needs to confirm it. |
@tobiasschweizer you need to sign it with the same account you used to make the git commits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tobiasschweizer try running the following git command:
git config user.email
and make sure you've signed with that email
src/lib/datepicker/calendar.ts
Outdated
* Default header of a [MatCalendar]. | ||
*/ | ||
@Component({ | ||
selector: 'default-header', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mat-calendar-header
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 3467c0a
src/lib/datepicker/calendar.ts
Outdated
@@ -64,6 +77,13 @@ import {Directionality} from '@angular/cdk/bidi'; | |||
changeDetection: ChangeDetectionStrategy.OnPush, | |||
}) | |||
export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges { | |||
|
|||
/** An input indicating the type of the custom header component, if set. */ | |||
@Input() customCalendarHeaderComponent: ComponentType<any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets just call the input headerComponent
(we know its for the calendar since we're in the calendar component and the "custom" doesn't add much useful info for the user)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 3467c0a
src/lib/datepicker/calendar.ts
Outdated
@Input() customCalendarHeaderComponent: ComponentType<any>; | ||
|
||
/** A portal containing the header for this calendar. */ | ||
calendarHeaderPortal: Portal<any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name this with a leading _
to indicate internal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 3467c0a
I renamed it as you suggested, made it private and added a getter method since the template needs to access it. To my understanding, private members are not accessible from the component's template (although this would make sense, conceptually speaking): https://stackoverflow.com/questions/34574167/angular2-should-private-variables-be-accessible-in-the-template?rq=1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than a getter method just name it with an _
and leave it public. We use this convention throughout Angular Material to mean "we had to leave this public due to technical constraints, but you shouldn't mess with it". We also automatically strip _
properties when generating our docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it!
done in b6938f2
src/lib/datepicker/calendar.ts
Outdated
@@ -200,6 +220,17 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges { | |||
} | |||
|
|||
ngAfterContentInit() { | |||
|
|||
if (this.customCalendarHeaderComponent !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just simplify this logic to:
this.clalendarHeaderPortal = new ComponentPortal(this.calendarHeaderComponent || MatCalendarHeader);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 3467c0a
In general I try to be as specific as possible and not rely on implicit type conversions. JavaScript may behave very unexpectedly in some cases.
This is my understanding of the construct:
If this.calendarHeaderComponent
is set, we expect it to be evaluated to true and thus returned. If not (undefined
), to false and thus MatCalendarHeader
to be returned.
src/lib/datepicker/datepicker.ts
Outdated
@@ -115,6 +115,10 @@ export class MatDatepickerContent<D> implements AfterContentInit { | |||
preserveWhitespaces: false, | |||
}) | |||
export class MatDatepicker<D> implements OnDestroy { | |||
|
|||
/** An input indicating the type of the custom header component for the calendar, if set. */ | |||
@Input() customCalendarHeaderComponent: ComponentType<any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calendarHeaderComponent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 014777a
src/lib/datepicker/calendar.html
Outdated
@@ -1,4 +1,7 @@ | |||
<div class="mat-calendar-header"> | |||
|
|||
<ng-template [cdkPortalOutlet]="calendarHeaderPortal"></ng-template> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
src/lib/datepicker/calendar.ts
Outdated
|
||
/** | ||
* Default header of a [MatCalendar]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** Default header for MatCalendar */
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 0faefc4
@@ -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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 0faefc4
@mmalerba Thanks, that looks better now ;-) |
@mmalerba Ok, everything is green now. Thx.! |
src/lib/datepicker/calendar.ts
Outdated
private _destroyed = new Subject<void>(); | ||
|
||
constructor(private _intl: MatDatepickerIntl, | ||
@Host() public calendar: MatCalendar<D>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tobiasschweizer I'm seeing a number of failures when trying to presubmit this related to injecting the calendar here. I think you may need to do this:
@Host() @Inject(forwardRef(() => MatCalendar)) public calendar: MatCalendar<D>
Since MatCalendarHeader
is declared before MatCalendar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests in Google's codebase for other project teams. I can't link to them since they're internal, I'll rerun the presubmit with the fix though.
In the mean time, it looks like you need to resolve some conflicts again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, merge done in ba4e839
# Conflicts: # src/lib/datepicker/datepicker-module.ts # src/lib/datepicker/datepicker.ts
<div class="custom-header"> | ||
<button mat-icon-button (click)="previousClicked('year')"><<</button> | ||
<button mat-icon-button (click)="previousClicked('month')"><</button> | ||
<span class="custom-header-label">{{periodLabel}}</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn’t be only 2 spaces?
|
||
get periodLabel() { | ||
let year = this._dateAdapter.getYearName(this._calendar.activeDate); | ||
let month = (this._dateAdapter.getMonth(this._calendar.activeDate) + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both variables could be const
.
flex: 1; | ||
text-align: center; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra new line.
}) | ||
export class MatCalendarHeader<D> implements OnDestroy { | ||
/** Subject that emits when the component has been destroyed. */ | ||
private _destroyed = new Subject<void>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn’t it be readonly
, @jelbourn?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have seen some of the _destroyed
subjects in our code marked readonly
, but to me it feels unnecessary for private
properties.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since #9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since #9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since #9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since #9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
…ontent This is something I ran into while working on aligning the datepicker with the most-recent Material design spec. Since angular#9639 we use a portal outlet to render the calendar header. The portal outlet directive will detach in `ngOnDestroy` and it won't wait for the parent animation to finish, which ends up shifting the entire calendar up while it's animating away. The only reason that this isn't visible at the moment is because the current animation isn't configured correctly, which causes it to go to `opacity: 0` immediately.
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This PR provides a new input for DatePicker so that a custom header component can be added to mat-calendar.
I intend to add a custom header from which date formats can be converted (see #2519).