Details Component — Dynamic Child Components — Digitteck
Details Component — Dynamic Child Components
frontend·7 June 2021·5 min read

Details Component — Dynamic Child Components

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:

xml
<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:

typescript
export interface ICxOverviewRow {
  label: string;
  value: string;
}
Final rendered component showing label/value rows in a table

cx-overview-container

The container renders a bare <table> with a named view container ref where rows will be dynamically inserted:

xml
<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:

typescript
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:

javascript
:host {
  display: table-row;
}

Two cells — label and value — rendered inside the host row:

xml
<td>
  <span>{{ row.label }}</span>
</td>
<td>
  <ng-container #body></ng-container>
</td>
typescript
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.

xml
<ng-template #content>
  <tr cx-overview-row [row]="row"></tr>
</ng-template>
typescript
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;
  }
}

Tags

AngularTypeScriptTemplate
digitteck

© 2026 Digitteck