Experimenting With Dynamic Template Rendering In Angular 2 RC 1

Before we dive in, I just want to clearly state that this is an experiment. I am still very much learning about how view containers and templates works. And, I have to give a huge Thank You to Pawel Kozlowski who painstakingly helped me wrap my head around the hierarchy of view containers in an Angular 2 application. It may be "simple"; but, it's not necessarily straightforward, especially coming from an Angular 1.x transclusion context.

That said, one thing I really wanted to be able to do in Angular 2 was pass-in a dynamic template - or a "Renderer" if you will - into another component. So, if you had a component that, among other things, rendered an "ngFor" internally, the calling context would be able to provide the implementation details for the body of said ngFor repeater. After my conversation with Pawel, and much trial and error, I think I have a proof-of-concept working.

NOTE: The ngFor directive can actually accept an external TemplateRef; but, that wasn't the point of this experiment. This was about template rendering mechanics - not an exploration of the ngFor directive.

Run this demo in my JavaScript Demos project on GitHub.

To explore this concept, I created a DynamicRepeater component. This component has a header, a footer, and a body. The header and footer are static; but, the body is composed of an ngFor directive that iterates over the component's [items] property. The primary template for the ngFor is provided by the DynamicRepeater; but, the internal content of the ngFor needs to be provided by the calling context in the form of a "<template>" tag.

To see how this is used, let's take a look at the AppComponent. In the following code, notice that the we're nesting a template element inside of the "dynamic-repeater" element. I am "tagging" this template with an "#itemRenderer" handle so that the DynamicRepeaterComponent can query for it as a ContentChild.

// Import the core angular services. import { Component } from "@angular/core"; // Import the application components and services. import { DynamicRepeaterComponent } from "./dynamic-repeater.component"; @Component({ selector: "my-app", directives: [ DynamicRepeaterComponent ], // In this view, we're passing a dynamic TemplateRef to the DynamicRepeater // component. We're not passing it in like a property; rather, we're "tagging" it // with the "#itemRenderer" handle. Then, the DynamicRepeater is going to query its // content (via ContentChild) for the template reference. When this TemplateRef is // "stamped out", it will make several local view variables available: // -- // * index // * item // -- // Here, you can see that the template is hooking into those variables using the // "let" syntax, ex. "let-color=item". template: ` <dynamic-repeater [items]="colors"> <template #itemRenderer let-color="item" let-index="index"> <div title="Item {{ index }}" class="swatch" [style.backgroundColor]="color.hex"> <br /> </div> <div class="name"> {{ color.name }} </div> </template> </dynamic-repeater> ` }) export class AppComponent { // I hold the collection of colors that will be rendered by the DynamicRepeater. public colors: any[]; // I initialize the component. constructor() { this.colors = [ { hex: "#E50000", name: "Red" }, { hex: "#FF028D", name: "Hot Pink" }, { hex: "#FF81C0", name: "Pink" }, { hex: "#FFD1DF", name: "Light Pink" }, { hex: "#FFB07C", name: "Peach" }, { hex: "#FF796C", name: "Salmon" } ]; } }

When the DynamicRepeater clones the template inside the internal ngFor loop, it will make several local view variables available: index and item. Our #itemRenderer template can then leverage those view-local variables by using the "let" syntax. In this case, you can see that we're mapping "item" to "color" and then referencing "color" within our template body.

Internally, the DynamicRepeater queries for the ContentChild, "#itemRenderer" and then passes it into a "template renderer" inside of its own ngFor loop. This "template renderer" is a custom directive that accepts both a TemplateRef and a context and then clones the given TemplateRef into the current ViewContainerRef.

In the following DynamicRepeater component, you'll notice that I'm actually using a function - createTemplateRenderer() - to provide the template renderer directive to the DynamicRepeater view. This function dynamically generates a template renderer that exposes the given properties as sub-properties off of the "context". So, while the template renderer can accept a plain [context] object input, this factory function allows you to create inputs that are more intuitively name-spaced, like [context.item] and [context.index].

// Import the core angular services. import { Component } from "@angular/core"; import { ContentChild } from "@angular/core"; import { TemplateRef } from "@angular/core"; // Import the application components and services. import { createTemplateRenderer } from "./template-renderer.directive"; @Component({ selector: "dynamic-repeater", inputs: [ "items" ], // Here, we are querying for the <template> tags in the content. queries: { itemTemplateRef: new ContentChild( "itemRenderer" ) }, // We're going to provide a dynamically-generated directive that exposes custom // inputs that we want to pass to our item renderer. In this case, we want to // expose "context.item" and "context.index". This will return a directive with // the selector, "template[render]", which are using in our view. directives: [ createTemplateRenderer( "item", "index" ) ], template: ` <header> <h2> Dynamic Repeater View </h2> </header> <dynamic-repeater-body> <dynamic-repeater-item *ngFor="let item of items; let index = index ;"> <template [render]="itemTemplateRef" [context.item]="item" [context.index]="index"> </template> </dynamic-repeater-item> </dynamic-repeater-body> <footer> <p> You have {{ items?.length }} item(s) being rendered. </p> </footer> ` }) export class DynamicRepeaterComponent { // I hold the items to render in our repeater. // -- // NOTE: Injected property. public items: any[]; // I hold the template used to render the item. // -- // NOTE: Injected query. public itemTemplateRef: TemplateRef<any>; // I initialize the component. constructor() { this.items = []; this.itemTemplateRef = null; } }

Now, let's look at the dynamically generated template renderer. Once you get past the "factory" nature of it, the logic of the component is actually quite simple. It injects the ViewContainerRef and then clones the TemplateRef input using the context input. Remember, in this particular case, the "context" contains the individually-bound "index" and "item" inputs from the DynamicRepeater component.

// Import the core angular services. import{ Directive } from "@angular/core"; import{ OnInit } from "@angular/core"; import{ TemplateRef } from "@angular/core"; import{ ViewContainerRef } from "@angular/core"; // I generate Class definitions that exposes custom sub-properties off the "context" // namespace. This class always exposes: // -- // * render (aliased as "template") // * context // -- // ... however, you can additionally provide other sub-properties of "conext" to make // the binding syntax easier to read. export function createTemplateRenderer( ...propertyNames: string[] ) { // Let's convert the incoming sub-property names into namespaced inputs off the // "context" object. For example, convert "foo" into "context.foo". var contextProperties = propertyNames.map( function operator( propertyName: string ) : string { return( "context." + propertyName ); } ); @Directive({ selector: "template[render]", inputs: [ "template: render", "context", ...contextProperties ] }) class TemplateRendererDirective implements OnInit { // I hold the context that will be exposed to the embedded view. // -- // NOTE: The context is an injectable input. However, it's sub-properties are // also individually injectable properties based on the arguments passed to the // factory function. public context: any; // I hold the TemplateRef that we are cloning into the view container. public template: TemplateRef<any>; // I hold the view container into which we are injecting the cloned template. public viewContainerRef: ViewContainerRef; // I initialize the directive. constructor( viewContainerRef: ViewContainerRef ) { this.context = {}; this.viewContainerRef = viewContainerRef; } // --- // PUBLIC METHODS. // --- // I get called once, when the class is initialized, after the inputs have been // bound for the first time. public ngOnInit() : void { if ( this.template && this.context ) { this.viewContainerRef.createEmbeddedView( this.template, this.context ); } } } // Return the dynamically generated class. return( TemplateRendererDirective ); }

When all is said and done, when we run this code, we get the following page output:

As you can see, the template body, provided by the AppComponent, was used to render the inner content of each "dynamic-repeater-item" within the DynamicRepeaterComponent's ngFor loop. Hella sweet!

This was just an experiment; but, I think this taps into a real need in the Angular 2 ecosystem. For static elements, projecting nodes with "ng-content" is great. But, when the "projection" of said content needs to be more dynamic, we need a way to provide custom renderers. And, while this experiment might not be perfect, I think it's definitely heading in the right direction.

Tweet This Deep thoughts by @BenNadel - Experimenting With Dynamic Template Rendering In Angular 2 RC 1 Woot woot — you rock the party that rocks the body!







