Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Paul Klinkenberg
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Paul Klinkenberg@frinky )

Templates Appear To Maintain Lexical Bindings In Angular 2 RC 1

By Ben Nadel on

Yesterday, when I was experimenting with dynamic template rendering in Angular 2, I happend to notice that the ngFor directive can accept an external TemplateRef. Not only did I want to experiment with this ngFor feature; but, it got me generally thinking about how a template maintains its bindings when its passed out-of-scope. From what I can see, it looks like a TemplateRef maintains the lexical binding to both its defining View and its defining Component instance.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To explore this idea, I created a completely trite component that does nothing more than proxy an ngFor directive. It accepts an [items] input and a [template] input and then uses both of these bindings to render an ngFor list.

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { TemplateRef } from "@angular/core";
  •  
  • @Component({
  • selector: "item-list",
  • inputs: [ "items", "template" ],
  •  
  • // In this View, notice that we are passing in a [ngForTemplate] to our ngFor
  • // directive. In doing so, the ngFor directive will use this template for rendering
  • // instead of its own TemplateRef.
  • // --
  • // NOTE: The ngFor directive will still use the same context object when rendering
  • // the externally-provided TemplateRef. As such, the external template can still use
  • // local view variables like "let-index" and "let-even".
  • template:
  • `
  • <ul>
  • <template ngFor [ngForOf]="items" [ngForTemplate]="template"></template>
  • </ul>
  • `
  • })
  • export class ItemListComponent {
  •  
  • // I hold the collection of items to render.
  • // --
  • // NOTE: Injected input value.
  • public items: any[];
  •  
  • // I hold the externally provided TemplateRef to render in the ngFor repeater.
  • // --
  • // NOTE: Injected input value.
  • public template: TemplateRef<any>;
  •  
  •  
  • // I initialize the component.
  • constructor() {
  •  
  • // ... nothing to do here.
  •  
  • }
  •  
  • }

Notice that the ngFor directive is accepting an [ngForTemplate] property. Internally, the ngFor directive will then use this passed-in TemplateRef input instead of its own dependency-injected TemplateRef when creating the embedded views.

In the root component, I then defined a <template> element and gave it a view-local reference that I could pass into the ItemListComponent. In the following code, take a careful look at what I'm referencing from within the template.

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { ItemListComponent } from "./item-list.component";
  •  
  • export interface IFriend {
  • id: number;
  • name: string;
  • };
  •  
  • @Component({
  • selector: "my-app",
  • directives: [ ItemListComponent ],
  •  
  • // In the following view, we're defining a TemplateRef and then passing it into the
  • // <item-list> component. Notice, however, that the template is making references to
  • // both local-view values (ie, let-X) as well as lexically-bound values to the root
  • // component (ie, this.bestFriend) and the component template (ie, #bffMessage). Even
  • // when the TemplateRef gets passed out-of-scope, it can still reference the
  • // lexically-bound values from whence it was defined.
  • template:
  • `
  • <input #bffMessage type="hidden" value="Woot - BFF!" />
  •  
  • <template #friendTemplate let-friend>
  •  
  • <li>
  • {{ friend.name }}
  •  
  • <template [ngIf]="( friend.id === bestFriend.id )">
  • &mdash; <strong>{{ bffMessage.value }}</strong>
  • </template>
  • </li>
  •  
  • </template>
  •  
  • <item-list
  • [items]="friends"
  • [template]="friendTemplate">
  • </item-list>
  • `
  • })
  • export class AppComponent {
  •  
  • // I hold the best friend (which is a reference to an item in the collection).
  • public bestFriend: IFriend;
  •  
  • // I hold the collection of friends to render.
  • public friends: IFriend[];
  •  
  •  
  • // I initialize the component.
  • constructor() {
  •  
  • this.friends = [ "Sarah", "Kim", "Tricia", "Lisa", "Joanna" ].map(
  • function iterator( name: string, index: number ) : IFriend {
  •  
  • return({
  • id: index,
  • name: name
  • });
  •  
  • }
  • );
  •  
  • this.bestFriend = this.friends[ 1 ];
  •  
  • }
  •  
  • }

As you can see, not only am I using the view-local variables exposed by the ngFor directive context (ex, let-friend), I'm also referencing a sibling element (#bffMessage) and a property on the root component itself (this.bestFriend).


 
 
 

 
 Lexically bound template references in Angular 2 RC 1. 
 
 
 

When we run this code, everything just works. Seamlessly.


 
 
 

 
 Lexically bound references from within TemplateRefs work in Angular 2 RC 1. 
 
 
 

This is really quite awesome. Especially since we can pass templates into other components for dynamic rendering. The fact that those passed-in templates maintain lexical binding to their defining components makes them much more usable.




Reader Comments

@Ben,

Cool stuff!

This idea (minus local template variables) was possible in angular 1 too.

Basically it doesn't matter what is the source of the lexical bind, view or component, it's a mechanism to bind properties from the "outside world"

In angular 1, you would have used a directive and $compile it with a $scope, since you can move objects around the $scope can be anything, same behaviour.

In Angular 2 templates are a first class citizen with their a special component (<template>) which makes things works a bit different but with same outcome.
Also, in angular 2 the compiler was abstracted away, to support offline compilation so we can't do it the old way (and also can't create NEW component types dynamically.)

Reply to this Comment

So is this just like passing a function to another function?

I mean that when that passed function being executed, it executes in a context it was defined, but also is able to accept some other data via arguments from the execution context.

Reply to this Comment

@Shlomi,

Right, good point. It's actually been a while since I've played with NG1 (since reading up on all this NG2 stuff); so, my mental model is starting to slip. I know that when you set the "transclude: true | element", the content of the component was pre-compiled and linked to the lexical scope... and then provided to your component as a packaged linking function .... or something like that :D I don't remember all the details off hand.

The <template> stuff in NG2 is pretty cool. Especially now that I'm starting to understand how much of a first-class citizen it is.

Reply to this Comment

@Anton,

Yeah, I think that is a pretty legitimate comparison. The template maintains the lexical bindings. However, when the ViewContainerRef needs to clone / insert the template instances, it can provide a "context" variable that exposes "let-enabled" values as well.

Reply to this Comment

Hi Ben,
have You been trying to provide data binding in standard way (angular templating) for dynamically created components?

so, there is a very long discussion about it https://github.com/angular/angular/issues/6223 and just to minimize your time for reading all that lagacy stuff i show an example which is the only one way (IMO) to do that but only to dyrectly setting a Component instance properties - https://github.com/angular/angular/issues/6223#issuecomment-221940268

i wanna hope that there is some more simple and abstract way to automatically setup dynamically created component's @Input an @Output properties for data binding targets and sources... what do You think about it ?

Reply to this Comment

@Max,

I haven't played around with the Dynamic Component Loader yet. I briefly looked at it when investigating this problem; but, it didn't seem to be right fit, especially since I need to render an arbitrary template. As such, the ViewContainerRef / TemplateRef approach ended up being the right one.

Thanks for the link, though - it is something I plan to investigate.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.