Skip to content
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

Merged
merged 64 commits into from
Mar 22, 2018
Merged

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

merged 64 commits into from
Mar 22, 2018

Conversation

tobiasschweizer
Copy link
Contributor

@tobiasschweizer tobiasschweizer commented Jan 27, 2018

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).

@googlebot
Copy link

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. I signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address on your commit. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot. The email used to register you as an authorized contributor must be the email used for the Git commit.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

@googlebot googlebot added the cla: no PR author must sign Google's Contributor License Agreement: https://opensource.google.com/docs/cla label Jan 27, 2018
templateUrl: 'datepicker-demo.html',
styleUrls: ['datepicker-demo.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
moduleId: module.id,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@EdricChan03
Copy link
Contributor

@tobiasschweizer Could you sign the CLA?

@tobiasschweizer
Copy link
Contributor Author

I did on behalf of my organization (https://github.com/dhlab-basel). Probably my boss needs to confirm it.

@mmalerba
Copy link
Contributor

@tobiasschweizer you need to sign it with the same account you used to make the git commits

@mmalerba mmalerba self-assigned this Jan 29, 2018
Copy link
Contributor

@mmalerba mmalerba left a 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

* Default header of a [MatCalendar].
*/
@Component({
selector: 'default-header',
Copy link
Contributor

Choose a reason for hiding this comment

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

mat-calendar-header

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 3467c0a

@@ -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>;
Copy link
Contributor

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)

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 3467c0a

@Input() customCalendarHeaderComponent: ComponentType<any>;

/** A portal containing the header for this calendar. */
calendarHeaderPortal: Portal<any>;
Copy link
Contributor

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

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 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

Copy link
Contributor

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.

Copy link
Contributor Author

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

@@ -200,6 +220,17 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
}

ngAfterContentInit() {

if (this.customCalendarHeaderComponent !== undefined) {
Copy link
Contributor

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);

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 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.

@@ -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>;
Copy link
Contributor

Choose a reason for hiding this comment

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

calendarHeaderComponent

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 014777a

@@ -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.


/**
* 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

@@ -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

@tobiasschweizer
Copy link
Contributor Author

@mmalerba Thanks, that looks better now ;-)

@tobiasschweizer
Copy link
Contributor Author

@mmalerba Ok, everything is green now. Thx.!

private _destroyed = new Subject<void>();

constructor(private _intl: MatDatepickerIntl,
@Host() public calendar: MatCalendar<D>,
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mmalerba Good point, which test gave you the error?

fixed in 822dd56

Copy link
Contributor

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.

Copy link
Contributor Author

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

Tobias Schweizer added 2 commits March 21, 2018 21:19
# Conflicts:
#	src/lib/datepicker/datepicker-module.ts
#	src/lib/datepicker/datepicker.ts
<div class="custom-header">
<button mat-icon-button (click)="previousClicked('year')">&lt;&lt;</button>
<button mat-icon-button (click)="previousClicked('month')">&lt;</button>
<span class="custom-header-label">{{periodLabel}}</span>
Copy link
Contributor

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);
Copy link
Contributor

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;
}

Copy link
Contributor

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>();
Copy link
Contributor

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?

Copy link
Contributor

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.

@mmalerba mmalerba merged commit 4e1bb26 into angular:master Mar 22, 2018
@tobiasschweizer tobiasschweizer deleted the tobiasschweizer/calendar-header-comp branch March 27, 2018 13:59
crisbeto added a commit to crisbeto/material2 that referenced this pull request Jul 30, 2018
…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.
jelbourn pushed a commit that referenced this pull request Aug 23, 2018
…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.
jelbourn pushed a commit that referenced this pull request Aug 25, 2018
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 27, 2018
…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.
vivian-hu-zz pushed a commit that referenced this pull request Sep 27, 2018
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Sep 28, 2018
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Oct 2, 2018
…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.
vivian-hu-zz pushed a commit that referenced this pull request Nov 6, 2018
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Dec 13, 2018
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request Jan 2, 2019
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request May 30, 2019
…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.
crisbeto added a commit to crisbeto/material2 that referenced this pull request May 30, 2019
…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.
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants