In this example I want to show you how I decided to go about creating a component that renders information regarding some specific entity. In my design I have a list of properties and each property has a key and a value. The ideea is simple, I want a table that is properly formatted, keys aligned to the left, and all the values are in the second column of the table, aligned one under another. The catch is that values are different, so I needed a different representation for each value.
The solution is more complicated, I decided to dynamically inject the row types inside a container (table) component. Each row component provides a key (which is used by the container component for the first column of the table) and a value (a template ref containing the second column representation).
The final result would look something like this
The usage would be:
<cx-overview-container> <app-cx-overview-row-text #overviewrow [Key]="'Name'" [ValueText]="'What'"></app-cx-overview-row-text> <app-cx-overview-row-text #overviewrow [Key]="'Description'" [ValueText]="'What'"></app-cx-overview-row-text> <app-cx-overview-row-text #overviewrow [Key]="'Rank'" [ValueText]="'What'"></app-cx-overview-row-text> <app-cx-overview-row-content #overviewrow [Key]="'Rank'"> Hello Kitty </app-cx-overview-row-content> </cx-overview-container>
To make this happen, I need that every row I dynamically inject to return the Key and the Template to be used
export interface ICxOverviewRow{ Key: string; Value: TemplateRef; }
cx-overview-container
The container component is a table that searches for the rows to inject in it’s children. For each row that it finds, it uses a wrapper around it and it adds the found child in the wrapper. The wrapper is solved using the component factory.
The html file:
<table> <tbody> <ng-container #body></ng-container> </tbody> </table>
The ts file:
import { AfterViewInit, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ContentChildren, OnInit, QueryList, ViewChild, ViewContainerRef } from '@angular/core'; import {ICxOverviewRow} from '@app/modules/module-windowbase/controls/cx-overview-container/i-cx-overview-row'; import {CxOverviewRowComponent} from '@app/modules/module-windowbase/controls/cx-overview-row/cx-overview-row.component'; @Component({ selector: 'cx-overview-container', templateUrl: './cx-overview-container.component.html', styleUrls: ['./cx-overview-container.component.less'] }) export class CxOverviewContainerComponent implements OnInit, AfterViewInit { @ContentChildren('overviewrow') public Options = new QueryList<ICxOverviewRow>(); @ViewChild('body', {read: ViewContainerRef}) public Body: ViewContainerRef; public CreatedComponents: ComponentRef<any>[] = []; // templates public Templates: ICxOverviewRow[] = []; constructor( private cdr: ChangeDetectorRef, private componentFactoryResolver: ComponentFactoryResolver) { } ngOnInit(): void { } public ngAfterViewInit(): void { this.Options.changes.subscribe(() => { this.LoadOptions(); }); this.LoadOptions(); } private LoadOptions() { this.DisposeContent(); this.CreatedComponents = []; // store option templates for (const option of this.Options) { this.Templates.push(option); } // load options for (const option of this.Templates) { this.CreateOptionViewsAndLoad(option); } } private CreateOptionViewsAndLoad(option: ICxOverviewRow): void { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(CxOverviewRowComponent); const componentRef = this.Body.createComponent(componentFactory); componentRef.instance.Key = option.Key; componentRef.instance.Template = option.Value; componentRef.instance.Initialize(); this.CreatedComponents.push(componentRef); this.cdr.detectChanges(); } private DisposeContent(): void { if (this.Body) { this.Body.clear(); for (const view of this.CreatedComponents) { view.destroy(); } } this.cdr.detectChanges(); } }
cx-overview-row
This component is used by the container as a wrapper around the found children. This represents the actual table row. Because tbody requires a tr at the first level underneath the tbody tag to properly format the table, this component, this component has the style defined as display:table-row
The less file
:host{ display: table-row; } .property_name { font-family: "lato-regular"; color: #5585ae; padding-right: 5px; }
The html file
<td class="property_name"> {{Key}} </td> <td class="property_name">:</td> <td class="property_value"> <ng-container #body></ng-container> </td>
The ts file
import { AfterViewInit, ChangeDetectorRef, Component, EmbeddedViewRef, Input, OnDestroy, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; @Component({ selector: 'cx-overview-row', templateUrl: './cx-overview-row.component.html', styleUrls : ['./cx-overview-row.styles.less'] }) export class CxOverviewRowComponent implements OnInit, AfterViewInit, OnDestroy { @Input() public Key: string; @Input() public Template: TemplateRef<any>; @ViewChild('body', {read: ViewContainerRef}) public Body: ViewContainerRef; private Views: EmbeddedViewRef<any>[] = []; constructor(private cdr: ChangeDetectorRef) { } ngOnInit(): void { } public ngAfterViewInit(): void { this.Initialize(); } public Initialize() { if (this.Template && this.Body) { const view = this.Body.createEmbeddedView(this.Template); this.Views.push(view); this.cdr.detectChanges(); } } public ngOnDestroy(): void { if (this.Body) { this.Body.clear(); for (const view of this.Views) { if (!view.destroyed) { view.destroy(); } } } this.cdr.detectChanges(); } }
app-cx-overview-row-content
Now you can define multiple child components to display the row value. The following is an example
<ng-template #content> <ng-content></ng-content> </ng-template>
import {Component, Input, TemplateRef, ViewChild} from '@angular/core'; import {ICxOverviewRow} from '@app/modules/module-windowbase/controls/cx-overview-container/i-cx-overview-row'; @Component({ selector: 'app-cx-overview-row-content', templateUrl: './cx-overview-row-content.component.html', styleUrls: ['./cx-overview-row-content.component.less'] }) export class CxOverviewRowContentComponent implements ICxOverviewRow { @Input() public Key: string; @ViewChild('content') public Value: TemplateRef<any>; constructor() { } ngOnInit(): void { } }