16

Let's say I've got this simple list rendering component:

import {Input, Component } from 'angular2/core'

@Component({
  selector: 'my-list',
  template: `
      <div *ngFor='#item of items' (click)='onItemClicked(item)'>
          {{item}}
      </div>
  `
})
class MyList {
    @Input() items: string[];

    onItemClicked(item) { console.log('Item clicked:', item); }
}

I use it like this:

  <my-list [items]='myAppsItems'></my-list>

So far so good.

Next I decide I want the user to be able to supply his own template for the rendered items, so I change the component

@Component({
  selector: 'my-list',
  template: `
      <template ngFor [ngForOf]="items" [ngForTemplate]="userItemTemplate" (click)='onItemClicked(item)'>
      </template>
  `
})
class MyList {
    @Input() items: string[];
    @ContentChild(TemplateRef) userItemTemplate: TemplateRef;

    onItemClicked(item) { console.log('Item clicked:', item); }
}

And use it like this:

<my-list [items]='items'>
   <template #item>
        <h1>item: {{item}}</h1>
   </template>
</my-list>

This works only I don't bind any event handlers to the list items (plunker). If I try to bind to the click event, as I did in the first version of the component, Angular throws the following exception:

"Event binding click not emitted by any directive on an embedded template"

Here's a plunker showing that. You can delete the click binding and it'll work.

How do I fix this? I just want the user to be able to specify a template for a subordinate item which I'm going to iterate via ngFor, but I need to be able to bind handlers to those items.

Mud
  • 28,277
  • 11
  • 59
  • 92
  • Hmm, this is a good one... one thing worth noting - your plunks are both using beta-8, which is ~2 months outdated. Updating to the latest (beta-15) doesn't fix the bug, but I'd nonetheless suggest doing it – drew moore Apr 20 '16 at 01:47

2 Answers2

15

Been looking for an answer to this for a week now and I finally came up with a pretty decent solution. Instead of using ngForTemplate I would suggest using ngTemplateOutlet.

It is already described pretty well here: angular2 feeding data back to `<template>` from `[ngTemplateOutlet]`

The custom template for the list items is placed between the component tags:

<my-list>
  <template let-item="item">
    Template for: <b>{{item.text}}</b> ({{item.id}})
  </template>
</my-list>

And the component template:

<ul>
  <li *ngFor="let item of listItems" (click)="pressed(item)">
    <template 
      [ngTemplateOutlet]="template" 
      [ngOutletContext]="{
        item: item
      }">
    </template>
  </li>
</ul>

I made an example here: https://plnkr.co/edit/4cf5BlVoqzZdUQASVQaC?p=preview

Community
  • 1
  • 1
Bender
  • 151
  • 1
  • 3
  • Thank you for this! I just updated my code to this and got rid of my custom template rendering directive. FYI, you have two `[ngOutletContext]` bindings in your code. It only works because the second one is ignored. – Mud Oct 20 '16 at 18:13
  • @Mud You are very welcome! I noticed that it did not work in the Android stock browser or Firefox (android) without adding a internationalization polyfill. It will otherwise throw an error "Intl is not defined". – Bender Nov 02 '16 at 13:17
10

Item template is defined in App context, it is not clear how to attach it to my-list component context. I have create wrapper directive that handles template and its variables, directive is wrapped into div to capture events. It can be used like this:

@Directive({
    selector: '[ngWrapper]'
})
export class NgWrapper
{
    @Input()
    private item:any;

    private _viewContainer:ViewContainerRef;

    constructor(_viewContainer:ViewContainerRef)
    {
        this._viewContainer = _viewContainer;
    }

    @Input()
    public set ngWrapper(templateRef:TemplateRef)
    {
        var embeddedViewRef = this._viewContainer.createEmbeddedView(templateRef);
        embeddedViewRef.setLocal('item', this.item)
    }
}
@Component({
  selector: 'my-list',
  directives: [NgWrapper],
  template: `
      <template ngFor #item [ngForOf]="items">
      <div (click)="onItemClicked(item)">
      <template [ngWrapper]="userItemTemplate" [item]="item"></template>
      </div>
      </template>
  `
})
class MyList {
    @Input() items: string[];
    @ContentChild(TemplateRef) userItemTemplate: TemplateRef;
    userItemTemplate1: TemplateRef;

    onItemClicked(item) {
        console.log('Item click:', item);
    }

    ngAfterViewInit(){
      this.userItemTemplate;
    }
}
@Component({
  selector: 'my-app',
  directives: [MyList],
  template: `
    <my-list [items]='items'>
      <template #item="item">
            <h1>item: {{item}}</h1>
       </template>
    </my-list>
  `
})
export class App {
  items = ['this','is','a','test']

      onItemClicked(item) {
        console.log('Item click:', item);
    }
}

The solution is not prerfect but nearly good, check plunkr.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
kemsky
  • 14,727
  • 3
  • 32
  • 51
  • See http://stackoverflow.com/questions/37225722/ng-content-select-bound-variable/37229495#37229495 for how to use `context` instead of the removed `setLocal` (based on this great answer) – Günter Zöchbauer May 14 '16 at 17:05
  • Great however this fails with RC4, please see the updated plunkr http://plnkr.co/edit/mFdVd9aORm2ciEfSni33?p=preview – Cagatay Civici Jul 06 '16 at 21:19
  • i would not recommend using this approach (or any other with similar complexity) until Angular 2 Final is released. API is not stable yet, so you may get more problems in nearest future. – kemsky Jul 07 '16 at 16:05
  • it changed a bit since the last release of angular2. There is not `setLocal` method in the `embeddedViewRef`. You pass the context as the second parameter. Another thing that changed is using the `let-item` syntax instead of `#item` on the template elements – Slava Shpitalny Aug 13 '16 at 19:10