14 Nov 2017
cat: Angular 2+
0 Comments

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!

Links

Be the first to write a comment.

Post A Comment