14

We are developing a corporate component library which should provide Material Designed Angular Components. So the users of this library are not supposed to use e.g. Angular Material directly but rather include some component like "custom-tabs".

Using the components of MatTabModule directly works like a charm, whereas when using our custom components the projected content does not show up.

Usage looks very similar to the Angular Material API:

<custom-tabs>
  <custom-tab [label]="labelA">Content A</custom-tab>
  <custom-tab [label]="labelB">Content B</custom-tab>
  <custom-tab [label]="labelC">Content C</custom-tab>
</custom-tabs>

The custom components try to project the content as follows:

<!-- custom-tabs template -->
<mat-tab-group>
  <ng-content></ng-content>
</mat-tab-group>

<!-- custom-tab template -->
<mat-tab [label]="label">
  <ng-content></ng-content>
</mat-tab>

Does anyone have an idea how we can get it working?

I provided a StackBlitz where you can see the problem in action.

5
  • For a start, the label input on your mat-tab component in custom-tab should have square brackets for binding. Commented Feb 17, 2018 at 16:50
  • @Edric Fair enough, although it won't get me closer to the solution of my problem unfortunately. ;) Commented Feb 18, 2018 at 23:16
  • Did you already find a solution for this problem? Thanks. Commented Jun 8, 2018 at 13:47
  • 2
    @dtodt Yes, just check the updated StackBlitz. Commented Jun 11, 2018 at 16:22
  • 1
    @RobertGruner It would be awesome if you would post the running code from StackBlitz as an answer. Commented Oct 25, 2019 at 13:52

5 Answers 5

9

material has changed the var names, so in the solution

tabs -> _allTabs

tabGroup -> _tabBodyWrapper

Sign up to request clarification or add additional context in comments.

1 Comment

You are a hero. Beer is on me
6

I think the issue you have is the same described in this github issue: https://github.com/primefaces/primeng/issues/1215

Basically the problem here is that ng-content does not provide @ContentChild when crossing component boundaries.

You can see that mat-tab uses @ContentChild: https://github.com/angular/components/blob/master/src/material/tabs/tab.ts#L56

So I think the only solution is to override it programmatically the way it is described in primeng issue.

3 Comments

Thanks a lot for the link! Guess we will try the suggestion anytime soon.
Really got it working this way. Thanks a alot @dariusz-ostolski! I updated the StackBlitz to show how it works. The trick is the AfterViewInit part in CustomTabsComponent. Don't know if this is the most elegant solution but it works for now.
I get the following error for this solution: No provider for InjectionToken MAT_TAB_GROUP! Anyone knows how to fix it?
1
it('should initialize tabGroup with tabs from ContentChildren', () => {
    const customTabDebugElements = fixture.debugElement.queryAll(By.directive(MockCustomTabComponent));
    const customTabs = customTabDebugElements.map(debugElement => debugElement.componentInstance as MockCustomTabComponent);
    
    // Simulate the ContentChildren query result
    component.tabs = new QueryList<CustomTabComponent>();
    component.tabs.reset(customTabs);
    
    // Call the lifecycle hook manually
    component.ngAfterViewInit();
    
    expect(component.tabGroup._tabs.length).toBe(customTabs.length);
  });

  it('should call ngAfterContentInit on tabGroup after setting tabs', () => {
    const customTabDebugElements = fixture.debugElement.queryAll(By.directive(MockCustomTabComponent));
    const customTabs = customTabDebugElements.map(debugElement => debugElement.componentInstance as MockCustomTabComponent);
    
    spyOn(component.tabGroup, 'ngAfterContentInit');
    
    // Simulate the ContentChildren query result
    component.tabs = new QueryList<CustomTabComponent>();
    component.tabs.reset(customTabs);
    
    // Call the lifecycle hook manually
    component.ngAfterViewInit();
    
    expect(component.tabGroup.ngAfterContentInit).toHaveBeenCalled();
  });

1 Comment

Thank you for posting this answer. While this code may answer the question, might you please edit your post to add an explanation as to why/how it works? This can help future readers learn and apply your answer. You are also more likely to get positive feedback (upvotes) when you include an explanation.
1
const customTabDebugElements = fixture.debugElement.queryAll(By.directive(MockCustomTabComponent));
const customTabs = customTabDebugElements.map(debugElement => debugElement.componentInstance as MockCustomTabComponent);

// Simulate the ContentChildren query result
component.tabs = new QueryList<CustomTabComponent>();
component.tabs.reset(customTabs);

// Call the lifecycle hook manually
component.ngAfterViewInit();

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
0
  it('should create the component', () => {
    expect(component).toBeTruthy();
  });

  it('should set _allTabs on the MatTabGroup with mapped MatTabs from tabs QueryList', () => {
    // If using mock tabs:
    // mockTabs.push(new TabComponent()); // Add mock TabComponents
    // component.tabs = new QueryList<TabComponent>(mockTabs);

    fixture.detectChanges(); // Trigger ngAfterViewInit

    expect(tabGroup._allTabs.length).toBe(mockTabs.length); // Adjust assertion based on logic
    mockTabs.forEach((tab, index) => {
      expect(tabGroup._allTabs.get(index)).toBe(tab.matTab());
    });
  });

  it('should call ngAfterViewInit on the MatTabGroup', () => {
    const ngAfterViewInitSpy = spyOn(tabGroup, 'ngAfterViewInit');

    fixture.detectChanges(); // Trigger ngAfterViewInit

    expect(ngAfterViewInitSpy).toHaveBeenCalled();
  });

  // Add more tests as needed, considering potential edge cases and error conditions
});

1 Comment

Thank you for posting this answer. While this code may answer the question, might you please edit your post to add an explanation as to why/how it works? This can help future readers learn and apply your answer. You are also more likely to get positive feedback (upvotes) when you include an explanation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.