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

fix(datepicker): allow MatCalendarHeader to be re-used inside a custom header #10856

Merged
merged 18 commits into from
Apr 19, 2018
Merged

fix(datepicker): allow MatCalendarHeader to be re-used inside a custom header #10856

merged 18 commits into from
Apr 19, 2018

Conversation

tobiasschweizer
Copy link
Contributor

@tobiasschweizer tobiasschweizer commented Apr 14, 2018

This PR makes MatCalendarHeader available for the use from the outside of MatCalendar. Like this, custom calendar header components may make use of the selector mat-calendar-header.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Apr 14, 2018
@tobiasschweizer
Copy link
Contributor Author

tobiasschweizer commented Apr 14, 2018

@mmalerba Where should the test case be added? I could make a custom header component that uses mat-calendar-header and add it to the datepicker demo (004fc44).

Or should it be a unit test in the spec file?

moduleId: module.id,
selector: 'custom-header2',
template: `
<div>Custom element</div>
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 Prepending elements may break the design (missing space at the bottom of the calendar body). Can this be solved with CSS?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that's a good point. I think for now if people are overriding the header they can just add CSS to make the popup taller as well. It would be good to see if we can change the Material CSS to make it work automatically though

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!

moduleId: module.id,
selector: 'custom-header2',
template: `
<div>Custom element</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that's a good point. I think for now if people are overriding the header they can just add CSS to make the popup taller as well. It would be good to see if we can change the Material CSS to make it work automatically though


<h2>Datepicker with custom header using standard header</h2>
<p>
<mat-form-field>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you create a unit test version of this that just tests that the component can be constructed without errors and the default header is in the DOM?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess src/lib/datepicker/datepicker.spec.ts would be a good place for that. I could move my test component CustomHeader2 there and create a test case where it is used as an input for the header.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that sounds good, you can leave it in the demo app too if you want, but the unit test is most important

class DatepickerWithCustomHeader {
@ViewChild('ch') datepicker: MatDatepicker<Date>;
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}
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

I tried to add the custom header component as an input:

@Component({
  template: `
    <input [matDatepicker]="ch">
    <mat-datepicker #ch [calendarHeaderComponent]="CustomHeaderForDatepicker"></mat-datepicker>
  `,
})
class DatepickerWithCustomHeader {
  @ViewChild('ch') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}

@Component({
  template: `
  <div>Custom element</div>
  <mat-calendar-header></mat-calendar-header>
  `
})
export class CustomHeaderForDatepicker {
  constructor() {
  }
}

But I get:

: Cannot determine the module for class CustomHeaderForDatepicker in /src/lib/datepicker/datepicker.spec.ts! Add CustomHeaderForDatepicker to the NgModule to fix it.

I remember that I had to add the custom header component to entryComponents for the demo page. How would that work for a test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Apr 17, 2018

Choose a reason for hiding this comment

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

Is this correct?

@Component({
  template: `
  <div>Custom element</div>
  <mat-calendar-header></mat-calendar-header>
  `
})
export class CustomHeaderForDatepicker {
  constructor() {
  }
}


@NgModule({
  imports: [],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  declarations: [CustomHeaderForDatepicker],
  entryComponents: [CustomHeaderForDatepicker]
})
export class CustomHeaderModule {}

Copy link
Contributor

Choose a reason for hiding this comment

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

You can modify the createComponent function to take an optional list of entryComponents. It already had optional lists for imports and providers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

entryComponents is not a member of the expected configuration object

static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed;
export declare type TestModuleMetadata = {
    providers?: any[];
    declarations?: any[];
    imports?: any[];
    schemas?: Array<SchemaMetadata | any[]>;
    aotSummaries?: () => any[];
};

Is this the way to do it angular/angular#10760 (comment) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the proposed solution would be

// Creates a test component fixture.
  function createComponent(
    component: any,
    imports: any[] = [],
    providers: any[] = [],
    entryComponents: any[] = []):
    ComponentFixture<any> {

    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        MatDatepickerModule,
        MatFormFieldModule,
        MatInputModule,
        NoopAnimationsModule,
        ReactiveFormsModule,
        ...imports
      ],
      providers,
      declarations: [component],
    });

    TestBed.overrideModule(BrowserDynamicTestingModule, {
      set: {
        entryComponents: [entryComponents]
      }
    }).compileComponents();

    return TestBed.createComponent(component);
  }

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Apr 17, 2018

Choose a reason for hiding this comment

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

I tried this with the modified createComponent function:

describe('datepicker with custom header', () => {
    let fixture: ComponentFixture<DatepickerWithCustomHeader>;
    let testComponent: DatepickerWithCustomHeader;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(
        DatepickerWithCustomHeader,
        [MatNativeDateModule],
        [],
        [CustomHeaderForDatepicker]
      );
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
    }));

    it('should instantiate a datepicker with a custom header', fakeAsync(() => {
      expect(testComponent).toBeTruthy();
    }));
  });

...

@Component({
  template: `
    <input [matDatepicker]="ch">
    <mat-datepicker #ch [calendarHeaderComponent]="CustomHeaderForDatepicker"></mat-datepicker>
  `,
})
class DatepickerWithCustomHeader {
  @ViewChild('ch') datepicker: MatDatepicker<Date>;
  @ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}

@Component({
  template: `
  <div>Custom element</div>
  <mat-calendar-header></mat-calendar-header>
  `
})
export class CustomHeaderForDatepicker {
  constructor() {
  }
}

However, I still get

: Cannot determine the module for class CustomHeaderForDatepicker in material2/src/lib/datepicker/datepicker.spec.ts! Add CustomHeaderForDatepicker to the NgModule to fix it.

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 maybe the fact that you're exporting the class is the problem. Did you try just

class CustomHeaderForDatepicker { ... }

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Apr 17, 2018

Choose a reason for hiding this comment

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

@mmalerba

done in 6aca934

However, there are still some issues:

  • tslint complains about import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing'; -> missing in rollup-globals.ts:

    ERROR: /home/travis/build/angular/material2/src/lib/datepicker/datepicker.spec.ts[33, 1]: Module "@angular/platform-browser-dynamic/testing" is missing from file /home/travis/build/angular/material2/tools/package-tools/rollup-globals.ts.

  • the test is executed now but fails. It complains about a missing import:

    1. should instantiate a datepicker with a custom header
      MatDatepicker datepicker with custom header
      Error: Component CustomHeaderForDatepicker is not part of any NgModule or the module has not been imported into your module. (line 22757)

    But if I add CustomHeaderForDatepicker as an import to createComponent it fails too

I would be glad for any advice ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -49,7 +56,13 @@ describe('MatDatepicker', () => {
...imports
],
providers,
declarations: [component],
declarations: declarations,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to take declarations in as a separate thing, you can just do:

declarations: [component, ...entryComponents]

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

}));

it('should instantiate a datepicker with a custom header', fakeAsync(() => {
expect(testComponent).toBeTruthy();
Copy link
Contributor

Choose a reason for hiding this comment

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

One more thing to check:

expect(fixture.debugElement.nativeElement.querySelector('mat-calendar-header')).toBeTruthy();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, it does not find it.

Do I need to do testComponent.datepicker.open(); before?

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Apr 18, 2018

Choose a reason for hiding this comment

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

I imagined a thing like this:

it('should find the standard header selector', fakeAsync(() => {
      testComponent.datepicker.open();
      fixture.detectChanges();
      expect(fixture.debugElement.nativeElement.querySelector('mat-calendar-header')).toBeTruthy();
    }));

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

But I had to use expect(document.querySelector('mat-calendar-header')).toBeTruthy();

I saw a lot of examples like this in the spec file.

Copy link
Contributor

Choose a reason for hiding this comment

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

Did you try fixture.nativeElement.querySelector(...) (with no debugElement)? I see this elsewhere in the file too. Using document.querySelector doesn't feel quite right...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know what you mean, you want to get the content from the parent element.

Unfortunately fixture.nativeElement.querySelector('mat-calendar-header') yields null.

Copy link
Contributor Author

@tobiasschweizer tobiasschweizer Apr 18, 2018

Choose a reason for hiding this comment

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

When I do console.log(fixture.nativeElement); I get

<div id="root1" ng-version="6.0.0-rc.5">
<input ng-reflect-mat-datepicker="[object Object]" aria-haspopup="true" aria-owns="mat-datepicker-1">
<mat-datepicker></mat-datepicker>
</div>

This corresponds with the selectors in the template of DatepickerWithCustomHeader. The selectors should be replaced by their associates templates so we could query for the actual DOM-elements.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I think I understand what's happening. The overlay must be adding itself to document.body which is outside the fixture. Ok document.querySelector is fine then

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 In case there are more things left to do for this PR, please let me know. I can take care of it tomorrow or Friday. Then I will be away for two weeks.

`,
})
class DatepickerWithCustomHeader {
@ViewChild('ch') datepicker: MatDatepicker<Date>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need these ViewChild since they aren't used for anything in the test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left the datepicker in 47acc75 for the moment

`
})
class CustomHeaderForDatepicker {
constructor() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can omit the constructor

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

expect(testComponent).toBeTruthy();
}));

it('should find the standard header selector', fakeAsync(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: 'should find the standard header element'

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 ea5e99b

@mmalerba mmalerba added pr: lgtm action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release labels Apr 18, 2018
@mmalerba mmalerba changed the title MatCalendarHeader: allow for use from the outside of MatCalendar fix(datepicker): allow MatCalendarHeader to be re-used inside a custom header Apr 18, 2018
@tobiasschweizer
Copy link
Contributor Author

@mmalerba There seem to be some issues with the integration tests (mode saucelabs required), I can see some failing for other PRs too. I do not think the failing tests are specifically related to this PR.

@mmalerba
Copy link
Contributor

yeah CI has been a little flaky lately, probably unrelated

component: any,
imports: any[] = [],
providers: any[] = [],
entryComponents: any[] = []): ComponentFixture<any> {
Copy link
Contributor

@rafaelss95 rafaelss95 Apr 18, 2018

Choose a reason for hiding this comment

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

nit: you could use better types instead of any:

import {Component, FactoryProvider, Type, ValueProvider, ViewChild} from '@angular/core';

...

function createComponent(
  component: Type<any>,
  imports: Type<any>[] = [],
  providers: (FactoryProvider | ValueProvider)[] = [],
  entryComponents: Type<any>[] = []): ComponentFixture<any> {
  ...
}

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

@Component({
template: `
<div>Custom element</div>
<mat-calendar-header></mat-calendar-header>
Copy link
Contributor

@rafaelss95 rafaelss95 Apr 18, 2018

Choose a reason for hiding this comment

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

Missing indentation for template?

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

@mmalerba mmalerba merged commit 153dfb8 into angular:master Apr 19, 2018
@tobiasschweizer tobiasschweizer deleted the tobiasschweizer/calendar_header branch April 19, 2018 20:01
@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: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants