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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support rendering templates selected with something else than only string id #288

Closed
znaczek opened this issue Jan 31, 2021 · 19 comments
Closed

Comments

@znaczek
Copy link

znaczek commented Jan 31, 2021

First of all - great library! 馃憤 Solves a lot of problems

I am using mockComponent functionality on dependant components and I am using ng-templates to customise it contents. Now I would like to test how this template renders.

In the docs we can find that on a mocked component we can use __render method. It accepts contentChildSelector: string as a selector. However in our project we are using directives in @ContentChild.

Example below (simplified for demonstration purpose):
Dependency TableComponent component:

@Component({
  selector: 'app-table',
  template: `
    <div *ngFor="let item of data">
      <ng-container
        *ngTemplateOutlet="cell.el; context: { $implicit: item }"
      ></ng-container>
    </div>`,
})
export class TableComponent {
  @ContentChild(CellDirective) public cell: CellDirective = null;
  @Input() public data: Array<any> = [];
}

CellDirective:

@Directive({
  selector: '[appCell]'
})
export class CellDirective {
  constructor(public el: TemplateRef<any> | null = null) {}
}

And AppComponent which uses TableComponent

@Component({
  selector: 'app-root',
  template: `
    <app-table
      [data]="data"
    >
      <ng-template appCell let-item>
        <div class="custom-data-element">Data: {{ item.data }}</div>
      </ng-template>
    </app-table>`,
})
export class AppComponent {
  public data = [
    {data: 1},
    {data: 2},
  ];
}

Now in the test I would like to see:

...
const tableComponent = fixture.debugElement.query(By.css('app-table')).componentInstance;

...

if (isMockOf(tableComponent, TableComponent, 'c')) {
      tableComponent.__render(???, { data: 'test data'});
      fixture.detectChanges();
      const dataText = fixture.debugElement.query(By.css('.custom-data-element'))
        .nativeElement.innerText;

      expect(dataText).toBe('Data: test data');
...

And whole point is that ???. The easiest here probably would be to use the directive selector [appCell] but anything would do the job as long as we could render templates selected by a directive rather then static string id.

Thanks to everybody for any participation!

@satanTime
Copy link
Member

satanTime commented Jan 31, 2021

Hi @znaczek,

thanks for the report. I've been looking for a case, you provided.

Btw, do you use read property of the decorator? I'm curious, because I found it lately, and I was thinking that it is a good example, where a string for __render is not enough to render its template, but I have not been able to find a proper code sample which uses it.

Next days I'll implement a fix for your case and let you know.

@satanTime
Copy link
Member

satanTime commented Jan 31, 2021

I think the interface could be like that:

component.__render(component.cell.el, ...), then it should solve all the issues and all the possible variety of providing templates.

What do you think?

@satanTime
Copy link
Member

Ah... it couldn't be so easy.... a mock CellDirective does not have el anymore. So the fix may take longer, but I'll found out a solution.

@satanTime
Copy link
Member

satanTime commented Jan 31, 2021

Hi @znaczek again,

your case can be tested like that without any adjustments.

describe('issue-288:mock', () => {
  beforeEach(() => MockBuilder(AppComponent, AppModule));

  it('renders the desired ContentChild', () => {
    const fixture = MockRender(AppComponent);

    const componentEl = ngMocks.find(TableComponent);
    const directive = ngMocks.findInstance(componentEl, CellDirective);
    // also possible
    // const directive = componentEl.componentInstance.cell;
    if (isMockOf(directive, CellDirective, 'd')) {
      directive.__render({ data: 3});
      fixture.detectChanges();
    }

    expect(fixture.nativeElement.innerHTML).toContain('<div class="custom-data-element">Data: 3</div>');
  });
});

The thing is like that:
CellDirective is a structural directive which contains a template.
Because CellDirective and TableComponent have been mocked - we should assert their interfaces, not their behavior (not to try to render the directive via the component).
Therefore we should render the template of the directive directly.

I'll try to adjust docs about the case and also to do additional research for possible cases.

@znaczek
Copy link
Author

znaczek commented Feb 1, 2021

Hi, @satanTime thanks for looking into this and extremely fast response!

So yes, I have tested it in my simplified example and it works indeed. And yes - we rather want to test what is inside of the ng-template that bears that directive rather then rendering in an already mocked component - otherwise why are we mocking that component?
So rendering the template directly seems a good approach here.

I think this issue can be closed. I will test this approach in a real project in the incoming days/weeks and eventually reopen if come across any obstacle.

However as you mentioned I think it worth adding somewhere in the docs because this pattern with ng-templates with directives as far as I can see is used by good amount of libraries.

Thanks

@znaczek znaczek closed this as completed Feb 1, 2021
@satanTime
Copy link
Member

That's great,

If you have time, I would like to ask you to post some code examples or links to libs which use this approach, especially which you want to use in your project.

I am working on enhancement and want to be sure that it will cover all possible cases.

Thank you in advance!

@znaczek
Copy link
Author

znaczek commented Feb 3, 2021

@satanTime For example ng-select.
Example class here
They use a lot of @ContentChild with directive as a selector.

And an example usage in my project:

<ng-template *ngIf="checkboxes" ng-option-tmp let-item="item" let-item$="item$" let-index="index">
  <div class="option-with-checkbox">
    <input id="item-{{ key }}-{{ index }}" type="checkbox" [ngModel]="item$.selected" />
    <div>{{ item | selectLabel: bindLabel }}</div>
  </div>
</ng-template>

@satanTime
Copy link
Member

Cool, thanks, I'll write docs including this example. Besides that I'll include mat-table and p-calendar.

@satanTime satanTime reopened this Feb 3, 2021
@satanTime
Copy link
Member

satanTime commented Feb 3, 2021

Hm... doesn't work for me with ng-select,

I was trying to fetch the directive like

ngMocks.findInstance(ngSelectEl, NgLabelTemplateDirective)

but NgLabelTemplateDirective isn't exported and there is no way to do it.
I think I need to add something like

const t1 = ngMocks.findTemplateRef(ngSelectEl, 'ng-label-tmp');
const t2 = ngMocks.findTemplateRef(ngSelectEl, Directive);
const t3 = ngMocks.findTemplateRef(ngSelectEl, '#id');
const debugEl1 = ngMocks.render(componentInstance, t1, letVar, tplVars);
const debugEl2 = ngMocks.render(componentInstance, [t2, t3], letVar, tplVars);

ngMocks.hide(componentInstance, t1);
ngMocks.hide(componentInstance, [t2, t3]);
ngMocks.hide(componentInstance);

@satanTime
Copy link
Member

satanTime commented Feb 3, 2021

I've implemented rendering based on properties, what do you think, would it fit?

  it('provides correct template for ng-label-tmp', () => {
    MockRender(TargetComponent);
    const ngSelect = ngMocks.findInstance(NgSelectComponent);

    if (isMockOf(ngSelect, NgSelectComponent, 'c')) {
      ngSelect.__render(['labelTemplate'], {}, {item: {name: 'test'}});
    }
    const tplEl = ngMocks.find('[data-prop="labelTemplate"]');
    expect(tplEl.nativeElement.innerHTML).toContain('<strong>test</strong>');
  });

What I do not like here is labelTemplate - a developer has to know or to investigate where the template is in the instance.

This was referenced Feb 4, 2021
@znaczek
Copy link
Author

znaczek commented Feb 4, 2021

I am not sure what's the implementation under the hood and if I understood your proposition correctly, but I would expect, that after:

fixture = TestBed.createComponent(TestedComponnent);
if (isMockOf(ngSelect, NgSelectComponent, 'c')) {
      ngSelect.__render(['labelTemplate'], {}, {item: {name: 'test'}});
    }

I can just do something like this:

expect(fixture.debugElement.nativeElement.innerHTML).toContain('<strong>test</strong>');
// or
const ngSelect = fixture.debugElement.queryAll(By.css('ng-select'));
expect(ngSelect.nativeElement.innerHTML).toContain('<strong>test</strong>');

So in above example I developer doesn't need to know what is inside either ng-select or any of it's template. I just check if the content of ng-template exists in the output html.

@satanTime
Copy link
Member

Almost right, and it will work like that.

The only thing is a dev should know that the labelTemplate property belongs to <ng-template ng-label-tmp let-item="item">.
In the same time, in Webstorm it is just a click on ng-label-tmp, not sure if VSC can the same.

@znaczek
Copy link
Author

znaczek commented Feb 4, 2021

Ok, that's true. So would that be a problem to so this:

ngSelect.__render('[ng-label-tmp]', {}, {item: {name: 'test'}});

I don't know the insights of the library implementation, but one thing led to another:
knowing the selector [ng-label-tmp] we could find the NgLabelTemplateDirective class. Then in the component on which __render method is called iterate through properties and find the one on which @ContentChild is set with NgLabelTemplateDirective as selector.

Not sure if it's even possible, just throwing ideas :p

@satanTime
Copy link
Member

yeah, I think that's feasible.
nobody should care about invisible directives and properties.

so it will be like that:

ngSelect.__render('idHere', {}, {item: {name: 'test'}});
ngSelect.__render('[attribute]', {}, {item: {name: 'test'}});

@satanTime
Copy link
Member

I've created a separate issue to track this: #292

@satanTime
Copy link
Member

This weekend a new release will be published, it contains a temp fix which allows to test ng-select.

@satanTime
Copy link
Member

satanTime commented Feb 14, 2021

Hi @znaczek,

the version has been released: 11.6.0.
Info how to test ng-select is here: https://ng-mocks.sudo.eu/guides/libraries/ng-select
info how to render TemplateRef: https://ng-mocks.sudo.eu/extra/templateref

Please let me know if it fits your expectations.
I'll leave a comment here, once there is a fix for a better approach.

Thank you in advance and happy coding!

@znaczek
Copy link
Author

znaczek commented Feb 23, 2021

Hi,
thanks for all, I have just downloaded the latest release and so far looks good.
Will let you know if find any other case.
Cheers

@satanTime
Copy link
Member

Hi @znaczek,

some improvements have been released:
https://ng-mocks.sudo.eu/guides/libraries/ng-select
https://ng-mocks.sudo.eu/api/ngMocks/formatHtml
https://ng-mocks.sudo.eu/api/ngMocks/reveal
https://ng-mocks.sudo.eu/api/ngMocks/render

now ng-mocks should provide full set of tools to render templates. If you still experience a lack of tools - ping me or create an issue.

Happy coding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants