When building a reusable details / overview component in Angular, a common challenge is injecting child row components that need to render inside a <table> as actual <tr> elements. Putting rows inside a wrapper div breaks table semantics, so Angular's ComponentFactoryResolver and ContentChildren are used to dynamically project each row into the correct container.
The component is used like this:
<cx-overview-container>
<app-cx-overview-row-content [row]="row" *ngFor="let row of dataRows">
</app-cx-overview-row-content>
</cx-overview-container>Each row is described by a simple interface:
export interface ICxOverviewRow {
label: string;
value: string;
}
cx-overview-container
The container renders a bare <table> with a named view container ref where rows will be dynamically inserted:
<table>
<tbody>
<ng-container #body></ng-container>
</tbody>
</table>After content initialises, it loops over all projected CxOverviewRowContentComponent instances and tells each one to render itself into the table body:
import { Component, ComponentFactoryResolver, ContentChildren, QueryList, ViewChild, ViewContainerRef, AfterContentInit } from '@angular/core';
import { CxOverviewRowContentComponent } from './cx-overview-row-content/cx-overview-row-content.component';
@Component({
selector: 'cx-overview-container',
templateUrl: './cx-overview-container.component.html',
styleUrls: ['./cx-overview-container.component.less']
})
export class CxOverviewContainerComponent implements AfterContentInit {
@ViewChild('body', { read: ViewContainerRef }) body: ViewContainerRef;
@ContentChildren(CxOverviewRowContentComponent) rows: QueryList<CxOverviewRowContentComponent>;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngAfterContentInit() {
this.rows.forEach(row => row.renderInto(this.body, this.componentFactoryResolver));
}
}cx-overview-row
The row component uses :host { display: table-row; } so the custom element itself acts as the <tr> without an extra wrapper:
:host {
display: table-row;
}Two cells — label and value — rendered inside the host row:
<td>
<span>{{ row.label }}</span>
</td>
<td>
<ng-container #body></ng-container>
</td>import { Component, Input, TemplateRef, ViewContainerRef, EmbeddedViewRef, ComponentFactoryResolver } from '@angular/core';
import { ICxOverviewRow } from '../../cx-overview-row.interface';
import { CxOverviewRowComponent } from '../cx-overview-row/cx-overview-row.component';
@Component({
selector: '[cx-overview-row]',
templateUrl: './cx-overview-row.component.html',
styleUrls: ['./cx-overview-row.component.less']
})
export class CxOverviewRowComponent {
@Input() row: ICxOverviewRow;
}app-cx-overview-row-content
This is the projection wrapper: the consumer uses it in the template, but its actual DOM output is a dynamically-created CxOverviewRowComponent injected into the container's view ref, not rendered in place.
<ng-template #content>
<tr cx-overview-row [row]="row"></tr>
</ng-template>import { Component, Input, TemplateRef, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentRef } from '@angular/core';
import { ICxOverviewRow } from '../cx-overview-row.interface';
import { CxOverviewRowComponent } from './cx-overview-row/cx-overview-row.component';
@Component({
selector: 'app-cx-overview-row-content',
templateUrl: './cx-overview-row-content.component.html'
})
export class CxOverviewRowContentComponent {
@Input() row: ICxOverviewRow;
@ViewChild('content') content: TemplateRef<any>;
renderInto(viewContainerRef: ViewContainerRef, resolver: ComponentFactoryResolver): void {
const factory = resolver.resolveComponentFactory(CxOverviewRowComponent);
const componentRef: ComponentRef<CxOverviewRowComponent> = viewContainerRef.createComponent(factory);
componentRef.instance.row = this.row;
}
}