Dynamically Loading The ng-Template From Its Name In Angular 9

Today we shall be looking at how we can dynamically load an ng-template from its name. I came accross this requriement when, in one of my projects,  my task was to load an ng-template from its name. My configuration was coming from a third party service. The configuration that was  returned contained the below info:
  1. {  
  2.     "field""address"//represents the field name  
  3.     "header""Address"// represents the Header of the Field  
  4.     "columnStyle""width:27%;"// represents the style to be applied on Table Cell  
  5.     "controlStyle""width:95%;"// represents the style to be applied on Control  
  6.     "order": 3, // Order of the Table cell  
  7.     "isVisible"true// whether the column is visible or not  
  8.     "ngTemplate""textBoxTemplate" // The template to be used  
  9. }  
Before we dive into loading the ng-template from its name, we'll just see how we can  load from its reference. For instance, if we have a sample template in our HTML like below
  1. <!--#labelTemplate is the Angular Template reference variable name-->  
  2. <ng-template #labelTemplate>  
  3.     <label>Some values goes here</label>  
  4. </ng-template>  
Now to reference this template in our HTML, we can simply use the below code snippet:
  1. <div class="row>  
  2.     <div class="col-12">  
  3.         <!--Here the labelTemplate is the template reference variable name given to our Template-->  
  4.         <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container>  
  5.     </div>  
  6. </div>  
NOTE
You can also pass context values to the ng-template if needed by using the ngTemplateOutletContext property.
 
Now coming to our requirement wherein we just have the ng-template name, if we use the above approach directly to load the template it won't work in our scenario, as the values are of type string & not template reference variable name. So how can we overcome this issue? For this what I did was I created a component wherein I'll keep all of my ng-templates & I'll decorate each of the ng-templates with my custom attribute directive.
 
Here is the code-snippet for my custom attribute directive, 
  1. import {  
  2.     Directive,  
  3.     Input,  
  4.     TemplateRef  
  5. } from '@angular/core';  
  6. @Directive({  
  7.     selector: '[appCtrlTemplate]'  
  8. })  
  9. export class CtrlTemplateDirective {  
  10.     @Input('appCtrlTemplate') name: string;  
  11.     constructor(public template: TemplateRef < any > ) {}  
  12. }  
Here is our sample code-snippet for my cell-template component, wherein we kept all of our ng-templates,
 
cell-template.component.html
  1. <ng-template #labelTemplate [appCtrlTemplate]="'labelTemplate'" let-rowData="rowData" let-col="col" let-rowIdx="rowIdx">  
  2.     <label [title]="rowData[col.field]">{{rowData[col.field]}}</label>  
  3. </ng-template>  
  4. <ng-template #textBoxTemplate [appCtrlTemplate]="'textBoxTemplate'" let-rowData="rowData" let-col="col" let-rowIdx="rowIdx">  
  5.     <mat-form-field [ngStyle]="col.controlStyle">  
  6.         <mat-label></mat-label>  
  7.         <input type="text" #inputText="ngModel" name="txt{{col.field}}{{rowIdx}}" matInput [(ngModel)]="rowData[col.field]" />  
  8.     </mat-form-field>  
  9. </ng-template>  
As you can see in our custom attribute directive we've one Input property named "appCtrlTemplate" & a public property templates of type TemplateRef<any>.
 
When we bind this custom attribute on our ng-template we pass the name of the template as property binding to it & the template reference is bound to template property of our custom attribute.
 
Below is the code-snippet for our cell-template.component.ts file, where we try to resolve the template reference from its name in getTemplate method. 
  1. import {  
  2.     Component,  
  3.     OnInit,  
  4.     ViewChildren,  
  5.     QueryList,  
  6.     Output,  
  7.     EventEmitter,  
  8.     TemplateRef  
  9. } from '@angular/core';  
  10. import {  
  11.     CtrlTemplateDirective  
  12. } from '../../directives/ctrl-template.directive';  
  13. @Component({  
  14.     selector: 'app-cell-templates',  
  15.     templateUrl: './cell-templates.component.html'  
  16. })  
  17. export class CellTemplatesComponent implements OnInit {  
  18.     @ViewChildren(CtrlTemplateDirective) templateRefs: QueryList < CtrlTemplateDirective > ;  
  19.     @Output() valueChanged: EventEmitter < any > = new EventEmitter < any > ();  
  20.     constructor() {}  
  21.     ngOnInit(): void {}  
  22.     getTemplate(templateName: string): TemplateRef < any > {  
  23.         return this.templateRefs.toArray().find(x => x.name.toLowerCase() == templateName.toLowerCase()).template;  
  24.     }  
  25. }  
So far we saw how we can resolve the template reference by its name. Let's practically try to see whether it works or not. For this I created a home.component.html page, where we've one mat-table in which we show some dummy employee data. This data is loaded from an employee-data-store.service which has some dummy records. Below is the code snippet for the same.
 
EmployeeDataStoreService.ts
  1. import {  
  2.     Employee  
  3. } from '../models/employee.model';  
  4. import {  
  5.     Injectable  
  6. } from '@angular/core';  
  7. @Injectable()  
  8. export class EmployeeDataStoreService {  
  9.     private eData: Employee[] = [];  
  10.     public getEmployees() {  
  11.         const json = `[  
  12. {  
  13.    "photo""/assets/img/user-icon.jpg",  
  14.    "age": 25,  
  15.    "name""Aimee Weeks",  
  16.    "gender""female",  
  17.    "email""aimeeweeks@codax.com",  
  18.    "dob""11/08/1988",  
  19.    "address""842 Java Street, Kapowsin, Mississippi, 8052"  
  20. },  
  21. {  
  22.    "photo""/assets/img/user-icon.jpg",  
  23.    "age": 22,  
  24.    "name""Vicky Avery",  
  25.    "gender""female",  
  26.    "email""vickyavery@codax.com",  
  27.    "dob""08/11/1988",  
  28.    "address""803 Vanderveer Street, Remington, South Carolina, 1829"  
  29. },  
  30. {  
  31.    "photo""/assets/img/user-icon.jpg",  
  32.    "age": 30,  
  33.    "name""Cleveland Vance",  
  34.    "gender""male",  
  35.    "email""clevelandvance@codax.com",  
  36.    "dob""12/21/1986",  
  37.    "address""397 Hamilton Walk, Loretto, Massachusetts, 1096"  
  38. },  
  39. {  
  40.    "photo""/assets/img/user-icon.jpg",  
  41.    "age": 40,  
  42.    "name""Craft Frost",  
  43.    "gender""male",  
  44.    "email""craftfrost@codax.com",  
  45.    "dob""02/02/1970",  
  46.    "address""519 Arlington Place, Waukeenah, Delaware, 4549"  
  47. },  
  48. {  
  49.    "photo""/assets/img/user-icon.jpg",  
  50.    "age": 23,  
  51.    "name""Debbie Blevins",  
  52.    "gender""female",  
  53.    "email""debbieblevins@codax.com",  
  54.    "dob""02/05/1980",  
  55.    "address""855 Hinckley Place, Edmund, Virginia, 6139"  
  56. },  
  57. {  
  58.    "photo""/assets/img/user-icon.jpg",  
  59.    "age": 27,  
  60.    "name""Woodard Lott",  
  61.    "gender""male",  
  62.    "email""woodardlott@codax.com",  
  63.    "dob""01/30/1982",  
  64.    "address""865 Karweg Place, Johnsonburg, Utah, 4270"  
  65. }  
  66. ]`;  
  67.         this.eData = JSON.parse(json);  
  68.         return this.eData;  
  69.     }  
  70. }  
And as I mentioned during the start of this article, my configuration is coming from some third party service, so in order to mimic this, I've created one configuration file which I'm loading info APP-INITIALIZER of our app.module.ts file.
 
Here is the code snippet for our configuraton file,
 
table-config.json
  1. {  
  2.     "columns": [{  
  3.         "field""photo",  
  4.         "header""Photo",  
  5.         "columnStyle""width:10%;",  
  6.         "controlStyle""width:50px;height:50px;",  
  7.         "order": 0,  
  8.         "isVisible"true,  
  9.         "ngTemplate""imgTemplate"  
  10.     }, {  
  11.         "field""name",  
  12.         "header""Name",  
  13.         "columnStyle""width:15%;",  
  14.         "order": 1,  
  15.         "isVisible"true,  
  16.         "ngTemplate""labelTemplate"  
  17.     }, {  
  18.         "field""dob",  
  19.         "header""DOB",  
  20.         "columnStyle""width:18%;",  
  21.         "order": 2,  
  22.         "isVisible"true,  
  23.         "ngTemplate""datePickerTemplate"  
  24.     }, {  
  25.         "field""address",  
  26.         "header""Address",  
  27.         "columnStyle""width:27%;",  
  28.         "controlStyle""width:95%;",  
  29.         "order": 3,  
  30.         "isVisible"true,  
  31.         "ngTemplate""textBoxTemplate"  
  32.     }, {  
  33.         "field""gender",  
  34.         "header""Gender",  
  35.         "columnStyle""width:18%;",  
  36.         "order": 4,  
  37.         "isVisible"true,  
  38.         "ngTemplate""radioTemplate"  
  39.     }, {  
  40.         "field""email",  
  41.         "header""Email",  
  42.         "columnStyle""width:12%;",  
  43.         "order": 5,  
  44.         "isVisible"true,  
  45.         "ngTemplate""textBoxTemplate"  
  46.     }]  
  47. }  
And now in our app.module.ts file,
  1. import {  
  2.     BrowserModule  
  3. } from '@angular/platform-browser';  
  4. import {  
  5.     NgModule,  
  6.     APP_INITIALIZER  
  7. } from '@angular/core';  
  8. import {  
  9.     HttpClientModule  
  10. } from '@angular/common/http';  
  11. import {  
  12.     BrowserAnimationsModule  
  13. } from '@angular/platform-browser/animations';  
  14. import {  
  15.     AppComponent  
  16. } from './app.component';  
  17. import {  
  18.     PagesModule  
  19. } from './pages/pages.module';  
  20. import {  
  21.     AppConfigService  
  22. } from './shared/services/app-config.service';  
  23. @NgModule({  
  24.     declarations: [  
  25.         AppComponent  
  26.     ],  
  27.     imports: [  
  28.         BrowserModule,  
  29.         HttpClientModule,  
  30.         BrowserAnimationsModule,  
  31.         PagesModule  
  32.     ],  
  33.     providers: [{  
  34.         provide: APP_INITIALIZER,  
  35.         useFactory: (appConfigSvc: AppConfigService) => {  
  36.             return async () => {  
  37.                 return appConfigSvc.getAppConfig().subscribe((response) => {  
  38.                     console.log('Response', response);  
  39.                 });  
  40.             }  
  41.         },  
  42.         deps: [AppConfigService],  
  43.         multi: true  
  44.     }],  
  45.     bootstrap: [AppComponent]  
  46. })  
  47. export class AppModule {}  
Finally our home.component.html file,
  1. <div class="row">  
  2.     <div class="col-12">  
  3.         <form #testForm="ngForm">  
  4.             <mat-card>  
  5.                 <mat-card-header>  
  6.                     <mat-card-title>NgTemplates</mat-card-title>  
  7.                     <mat-card-subtitle>Dynamically loading Templates from Template Name</mat-card-subtitle>  
  8.                 </mat-card-header>  
  9.                 <mat-card-content>  
  10.                     <table mat-table [dataSource]="dataSource" class="w-100">  
  11.                         <ng-container *ngFor="let col of columns" [matColumnDef]="col.field">  
  12.                             <th mat-header-cell *matHeaderCellDef>  
  13. {{col.header}}  
  14. </th>  
  15.                             <td mat-cell *matCellDef="let row; let idx=index" [ngStyle]="col.columnStyle">  
  16.                                 <ng-cotainer [ngTemplateOutlet]="getTemplate(col.ngTemplate)"  
  17. [ngTemplateOutletContext]="{rowData:row,col:col,rowIdx:idx}"></ng-cotainer>  
  18.                             </td>  
  19.                         </ng-container>  
  20.                         <tr mat-header-row *matHeaderRowDef="displayedColumns;"></tr>  
  21.                         <tr mat-row *matRowDef="let row; columns:displayedColumns; let idx = index"></tr>  
  22.                     </table>  
  23.                 </mat-card-content>  
  24.             </mat-card>  
  25.             <app-cell-templates #cellTemplates id="cellTemplates"></app-cell-templates>  
  26.         </form>  
  27.     </div>  
  28. </div>  
code-snippet for Home.component.ts file
  1. import {  
  2.     Component,  
  3.     OnInit,  
  4.     ViewChild,  
  5.     TemplateRef,  
  6.     OnDestroy  
  7. } from '@angular/core';  
  8. import {  
  9.     MatTableDataSource  
  10. } from '@angular/material/table';  
  11. import {  
  12.     takeUntil  
  13. } from 'rxjs/operators';  
  14. import {  
  15.     CellTemplatesComponent  
  16. } from '../../shared/component/cell-templates/cell-templates.component';  
  17. import {  
  18.     Employee  
  19. } from '../../shared/models/employee.model';  
  20. import {  
  21.     TableConfig,  
  22.     IColumn  
  23. } from '../../shared/models/table-config.model';  
  24. import {  
  25.     AppConfigService  
  26. } from '../../shared/services/app-config.service';  
  27. import {  
  28.     Subject  
  29. } from 'rxjs';  
  30. import {  
  31.     EmployeeDataStoreService  
  32. } from 'src/app/shared/services/employee-data-store.service';  
  33. import {  
  34.     AppUtility  
  35. } from 'src/app/app-utility';  
  36. @Component({  
  37.     selector: 'app-home',  
  38.     templateUrl: './home.component.html'  
  39. })  
  40. export class HomeComponent implements OnInit, OnDestroy {  
  41.     @ViewChild('cellTemplates') cellTemplates: CellTemplatesComponent;  
  42.     columns: IColumn[];  
  43.     displayedColumns: string[];  
  44.     tableConfig: TableConfig;  
  45.     dataSource: MatTableDataSource < Employee > ;  
  46.     private componentDestroyed$: Subject < boolean > ;  
  47.     constructor(private appConfigService: AppConfigService, private eStoreDataService: EmployeeDataStoreService) {  
  48.         this.displayedColumns = [];  
  49.         this.columns = [];  
  50.         this.dataSource = new MatTableDataSource([]);  
  51.         this.componentDestroyed$ = new Subject < false > ();  
  52.     }  
  53.     ngOnInit(): void {  
  54.         this.appConfigService.appConfiguration$.pipe(takeUntil(this.componentDestroyed$)).subscribe((response) => {  
  55.             if (response.columns && response.columns.length > 0) {  
  56.                 this.tableConfig = response;  
  57.                 this.initializeTable();  
  58.             }  
  59.         });  
  60.     }  
  61.     private initializeTable() {  
  62.         this.tableConfig.columns.map(x => {  
  63.             let obj: IColumn = {}  
  64.             as IColumn;  
  65.             Object.keys(x).forEach(prop => {  
  66.                 if (prop == "columnStyle" || prop == "controlStyle") {  
  67.                     obj[prop] = AppUtility.convertNgStyleStringToObject(x[prop].toString());  
  68.                 } else {  
  69.                     obj[prop] = x[prop];  
  70.                 }  
  71.             });  
  72.             this.columns.push(obj);  
  73.         });  
  74.         this.displayedColumns = this.columns.filter(x => x.isVisible).sort(x => x.order).map(x => x.field);  
  75.         this.dataSource.data = this.eStoreDataService.getEmployees();  
  76.     }  
  77.     getTemplate(ngTemplateName: string): TemplateRef < any > {  
  78.         return this.cellTemplates.getTemplate(ngTemplateName);  
  79.     }  
  80.     ngOnDestroy() {  
  81.         this.componentDestroyed$.next(true);  
  82.         this.componentDestroyed$.complete();  
  83.     }  
  84. }  
Finally here is our working demo, as we can see,  each of our template is rendered dynamically from its name.