Uh oh!
There was an error while loading. Please reload this page.
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
googlebot
commented
Jan 27, 2018
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.
Your formatting should be 2 spaces instead of a tab or four characters.
There was a problem hiding this comment.
@Chan4077 Thanks for pointing that out. I changed the indentation to two spaces, undoing the changes introduced previously thereby.
EdricChan03
commented
Jan 29, 2018
@tobiasschweizer Could you sign the CLA? |
tobiasschweizer
commented
Jan 29, 2018
I did on behalf of my organization (https://github.com/dhlab-basel). Probably my boss needs to confirm it. |
mmalerba
commented
Jan 29, 2018
@tobiasschweizer you need to sign it with the same account you used to make the git commits |
mmalerba
left a comment
There was a problem hiding this comment.
@tobiasschweizer try running the following git command:
git config user.email
and make sure you've signed with that email
| * Default header of a [MatCalendar]. | ||
| */ | ||
| @Component({ | ||
| selector: 'default-header', |
| 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.
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)
| @Input() customCalendarHeaderComponent: ComponentType<any>; | ||
| /** A portal containing the header for this calendar. */ | ||
| calendarHeaderPortal: Portal<any>; |
There was a problem hiding this comment.
name this with a leading _ to indicate internal
There was a problem hiding this comment.
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.
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.
| ngAfterContentInit() { | ||
| if (this.customCalendarHeaderComponent !== undefined) { |
There was a problem hiding this comment.
I think you can just simplify this logic to:
this.clalendarHeaderPortal=newComponentPortal(this.calendarHeaderComponent||MatCalendarHeader);There was a problem hiding this comment.
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.
| 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>; |
| @@ -1,4 +1,7 @@ | |||
| <div class="mat-calendar-header"> | |||
| <ng-template [cdkPortalOutlet]="calendarHeaderPortal"></ng-template> | |||
There was a problem hiding this comment.
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.
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:
<divclass="mat-calendar-controls"><buttonmat-buttonclass="mat-calendar-period-button"
(click)="_currentPeriodClicked()" [attr.aria-label]="_periodButtonLabel">
{{_periodButtonText}}
<divclass="mat-calendar-arrow" [class.mat-calendar-invert]="_currentView != 'month'"></div></button><divclass="mat-calendar-spacer"></div><buttonmat-icon-buttonclass="mat-calendar-previous-button"
[disabled]="!_previousEnabled()" (click)="_previousClicked()"
[attr.aria-label]="_prevButtonLabel"></button><buttonmat-icon-buttonclass="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.
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.
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.
| import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal'; | ||
| /** | ||
| * Default header of a [MatCalendar]. |
There was a problem hiding this comment.
/** Default header for MatCalendar */
| 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.
nit: continuation lines indented 2
tobiasschweizer
commented
Mar 21, 2018
@mmalerba Thanks, that looks better now ;-) |
tobiasschweizer
commented
Mar 21, 2018
@mmalerba Ok, everything is green now. Thx.! |
| private _destroyed = new Subject<void>(); | ||
| constructor(private _intl: MatDatepickerIntl, | ||
| @Host() public calendar: MatCalendar<D>, |
There was a problem hiding this comment.
@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.
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.
# 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.
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.
Both variables could be const.
| flex: 1; | ||
| text-align: center; | ||
| } | ||
| }) | ||
| 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.
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).