Angular 4 – Rendering Templates using ViewContainerRef
There times where we want our component to render custom template because we want it to be more dynamic just like most open-source libraries that want to cover more use cases as possible. Here is an example.
<rtable [rows]="books">
<rtable-cell header="Thumbnail"
type="image">
<ng-template #template
let-book>
<img src="/assets/{{ book.imageSrc }}">
</ng-template>
</rtable-cell>
<rtable-cell header="ISBN"
field="isbn"></rtable-cell>
<rtable-cell header="Title"
field="title"></rtable-cell>
<rtable-cell header="Date Published"
field="publish.datePublished"
type="date"></rtable-cell>
<rtable-cell header="Published By"
field="publish.publisherName"></rtable-cell>
<rtable-cell header="Author"
field="author"></rtable-cell>
<rtable-cell header="Price"
field="price"
type="money"></rtable-cell>
<rtable-cell header="Action">
<ng-template #template
let-book>
<button>Buy Now</button>
<button>Add to Wishlist</button>
</ng-template>
</rtable-cell>
</rtable>
This codes are from the previous blog post. <rtable-cell>
can render the fields automatically by just setting the field
attribute but we also want the user of our component to be able to put custom content.
In this post we will be finishing the responsive table from the last blog post. We will also discuss what is the purpose of #template
and let-book
keywords and the ViewContainerRef
.
Let’s Start
If you remembered from the previous post we have a src/app/components/shared/template-renderer.component.ts open that and add these codes.
import { TemplateRef } from "@angular/core";
export interface ITemplateProvider {
getTemplateReference: () => TemplateRef<any>;
}
It is just a simple interface. This is not required but I want to make my codes as clear as possible. The method getTemplateReference
returns TemplateRef
which is a reference to a template. This will make more sense later on when we use this interface and when we create a component that renders a template.
Open your responsive-table.component.ts and add these code.
import { Component, Input, ContentChildren, QueryList, ContentChild, TemplateRef } from "@angular/core";
import { ITemplateProvider } from "../shared/template-renderer.component";
@Component({
selector: 'rtable-cell',
template: ''
})
export class ResponsiveTableCellComponent implements ITemplateProvider {
@Input()
header: string;
@Input()
field: string;
@ContentChild('template', { read: TemplateRef }) template: TemplateRef<any>;
getTemplateReference() {
return this.template;
}
}
// Codes for ResponsiveTableComponent is not included for brevity
We added a new property template
with decorator @ContentChild
. Now ResponsiveTableCellComponent
now gets reference of any templates with a marker #template
but you can use any name. @ContentChildren
and @ContentChild
can take a type or string
. In the previous post I used type ResponsiveTableCellComponent
get all those <rtable-cell>
tags and now we just use string. Since we are implementing ITemplateProvider
we use the template
property and return it in getTemplateReference()
method.
Open template-renderer.component.ts again and add these codes.
import {
Component,
OnInit,
OnDestroy,
ViewContainerRef,
Input,
EmbeddedViewRef,
TemplateRef
} from "@angular/core";
// Codes for ITemplateProvider is not included for brevity
@Component({
selector: 'tpl-render',
template: ''
})
export class TemplateRendererComponent implements OnInit, OnDestroy {
@Input()
data: any;
@Input()
templateProvider: ITemplateProvider;
view: EmbeddedViewRef<any>;
constructor(
private viewContainer: ViewContainerRef
) {
}
ngOnInit(): void {
this.view = this.viewContainer.createEmbeddedView(
this.templateProvider.getTemplateReference(),
{
$implicit: this.data
}
);
}
ngOnDestroy(): void {
this.view.destroy();
}
}
This is the component that will render the templates. It requires you to pass an instance of ITemplateProvider
and the data
you want it to pass into the template.
ViewContainerRef
In the constructor we injected ViewContainerRef
so we get a reference to the view that contains the <tpl-render>
. <tpl-render>
does not have its own template because we will use ViewContainerRef
to append a new view to the view container. To append a view to the view container we use createEmbeddedView
. We pass it a TemplateRef
and the data that will be pass to the view, in this case the data
property.
$implicit
is a keyword used by Angular for passing data to a view variable that is explicitly declared, in this case it is the let-book
. You can also use other variable name and it will still contain what the $implicit
property contains e.g. let-foo
.
After the view is created we store the reference to our view
property so we can destroy it later on the life cycle hook called ngOnDestroy
.
Don’t forget to add TemplateRendererComponent
to your module.
Using the Renderer
We can now use the TemplateRendererComponent
to append any custom content to our responsive table. Open responsive-table.component.html and add these codes.
<div class="tbl-container">
<div class="tbl-head">
<div class="tbl-cell"
*ngFor="let c of columns">
{{ c.header }}
</div>
</div>
<div class="tbl-body">
<div class="tbl-row"
*ngFor="let r of rows;">
<div class="tbl-cell"
*ngFor="let c of columns">
<ng-template [ngIf]="c.template"> <!-- <- This is the ViewContainerRef of <tpl-render> -->
<tpl-render [data]="r"
[templateProvider]="c"></tpl-render>
</ng-template>
<ng-template [ngIf]="!c.template">
<div class="tbl-cell-value">
{{ parseDotNotation(r, c.field) }}
</div>
</ng-template>
</div>
</div>
</div>
</div>
As you can see, if a ResponsiveTableCellComponent
have a template
then we use <tpl-render>
. data
attribute will contain r
(the row data) and templateProvider
will contain the c
. c
at this point is an instance of ITemplateProvider
.
The components are working now but we need to fix some CSS and add the last feature which is the type
attribute of <rtable-cell>
to format data. Open responsive-table.component.ts and add this lines of codes in the ResponsiveTableCellComponent
.
export class ResponsiveTableCellComponent implements ITemplateProvider {
// Codes removed for brevity
@Input()
type: string = 'string';
// Codes removed for brevity
}
Now open responsive-table.component.html and add these codes. This is the final version of this file.
<div class="tbl-container">
<div class="tbl-head">
<div class="tbl-cell"
*ngFor="let c of columns">
{{ c.header }}
</div>
</div>
<div class="tbl-body">
<div class="tbl-row"
*ngFor="let r of rows;">
<div class="tbl-cell"
[ngClass]="{ 'tbl-cell-img': c.type == 'image' }"
*ngFor="let c of columns">
<ng-template [ngIf]="c.template">
<tpl-render [data]="r"
[templateProvider]="c"></tpl-render>
</ng-template>
<ng-template [ngIf]="!c.template">
<div class="tbl-cell-title">
{{ c.header }}
</div>
<div class="tbl-cell-value"
*ngIf="c.type == 'string'">
{{ parseDotNotation(r, c.field) }}
</div>
<div class="tbl-cell-value"
*ngIf="c.type == 'date'">
{{ parseDotNotation(r, c.field) | date:'longDate' }}
</div>
<div class="tbl-cell-value"
*ngIf="c.type == 'money'">
{{ parseDotNotation(r, c.field) | currency:'USD':'$' }}
</div>
</ng-template>
</div>
</div>
</div>
</div>
This last update is simple. First we added an [ngClass]
attribute that will add tbl-cell-img
class if the type
of the <rtable-cell>
is “image”. Then the second block of [ngIf]
now contains a bunch of [ngIf]
s blocks that shows different formats depending on type
.
This is it for this post. Thanks for reading!