I want to create a tab group that is defined in a child component, but where it's possible to create tabs (and their contents) from the parent component.
Initially, I tried to use content projection.
<child>
<mat-tab tab label="Hello">World!</mat-tab>
</child>
<mat-tab-group>
<mat-tab label="Some">Tab!</mat-tab>
<ng-content selector="[tab]"></ng-content>
</mat-tab-group>
But the content wasn't being rendered in the child because since the tabs aren't in a tab group, they're not being initialized. And like said here, ng-content
needs to know the contents of the projection at compile time, so it is suggested that we manually initialize the tabs, though with no useful explanation...
This answer, though related to mat-table
, leaves a good idea to try for my case.
@ViewChild(MatTable) tabGroup!: MatTabGroup;
@ContentChildren(MatTab) tabs!: QueryList<MatTab>;
ngAfterContentInit() {
this.tabs.forEach(tab => this.tabGroup./*🤷♂️*/);
}
However, the tab group doesn't support any methods related to changing the Tab list.
*ngFor
with content projectionThe only information every tab needs really is just the label and its content. We can avoid projecting the tab if we just project its inner contents. We just need the parent to send some meta-information down to the child.
child.component.ts
@Input() tabs!: {label: string, selector: string}[];
child.component.html
<mat-tab-group>
<!-- some default tabs -->
<mat-tab *ngFor="let tab of tabs" [label]="tab.label">
<ng-content [select]="tab.selector"></ng-content>
</mat-tab>
</mat-tab-group>
parent.component.ts
tabs = [{label: "Hello", selector: "hello"}];
parent.component.html
<child [tabs]="tabs">
<p hello>World!</p>
</child>
To be clear, the tabs.selector
property will be used by the parent component to mark the content to project, and by the child component's ng-content
. Since it must be kebab-case, it has to be independent from label
.
I thought this solution worked, but it turned out it just worked if there were only 2 tabs.
Any other suggestions?
ng-template
and TemplateRef
This solution, that actually works, is very similar to the last attempt I made.
All information transfer (the label and the template) relies on a helper directive, which makes this code almost not rely on TS. This section of the Angular docs helped a lot.
@Directive({ selector: '[my-tab]', inputs: ['label'], })
export class MyTabDirective {
label: string | undefined;
constructor(/*DI*/ public templateRef: TemplateRef<any>) { }
}
<mat-tab-group>
<mat-tab *ngFor="let tab of tabs" [label]="tab.label">
<ng-container [ngTemplateOutlet]="tab.templateRef"></ng-container>
</mat-tab>
</mat-tab-group>
<child>
<ng-template my-tab label="Tab 1">
<h1>Hello,</h1>
<p>Tab 1 content</p>
</ng-template>
<ng-template my-tab label="Tab 2">
<h1>World!</h1>
<p>Tab 2 content</p>
</ng-template>
</child>