7

I'm using Angular 2-rc3 and have a Component and I want to apply transclusion, just in a bit of a different way. Here's my component:

import { Component, Input } from '@angular/core';

@Component({
    selector: 'my-list',
    template: `<ul>
        <li *ngFor="let item of data">
            -- insert template here --
            <ng-content></ng-content>
        </li>
    </ul>`
})
export class MyListComponent {
    @Input() data: any[];
}

And I use it like this:

<my-list [data]="cars">
    <div>{{item.make | uppercase}}</div>
</my-list>

As you can see, I'm trying to define an inline template that will be used by my component. Now this goes horribly wrong. First off, a databinding exception saying it can't read property 'make' of undefined. It's trying to read item.make off of my surrounding component, not of the MyListComponent. But even if I temporarily disable this for now:

<my-list [data]="cars">
    <div>{item.make | uppercase}</div>
</my-list>

Then the second problem appears:

-- insert template here --
-- insert template here --
-- insert template here --
-- insert template here --
{item.make | uppercase}

So Angular doesn't actually copy the template for use within the *ngFor, it just binds the elements and they end up associated with the last item.

How do I get this to work?

I had the same issue with AngularJS, where petebacondarwin posted a solution to manipulate the DOM through compile, which was great. I have this option with Angular 2 as well by injecting ElementRef in my component, but! One big difference is that compile in AngularJS went off before databinding, meaning there were no problems using {{item.make}} in the template. With Angular 2, this seems to be a no-go since {{item}} is being parsed before-hand. So what's the best way to go about this? Using the slightly different notation [[item]] and string replacing the entire thing doesn't seem the most elegant way...

Thanks in advance!

// Edit: Here's a Plnkr that reproduces the problem.

J.P.
  • 5,567
  • 3
  • 25
  • 39
  • It would be great to have a Plunker that allows to reproduce where everything is in place how you want to use it. I find it hard to, for example, figure out what "insert template here" is supposed to mean exactly. – Günter Zöchbauer Jun 24 '16 at 17:10
  • @J.P.tenBerge I had a similar issue and [found a solution](http://stackoverflow.com/questions/38174837/how-to-create-a-component-with-a-dynamic-template-component-transclude-with-in) – Ross Jul 22 '16 at 17:31

2 Answers2

6

To spell out the ngForTemplate method:

(As of Angular 4 the element is now called <ng-template>.)

  1. Use a <template> tag in both the outer and inner components, instead of <ng-content>.

  2. The <li> is moved to the app.component html, and the <template> on this component has a special 'let-' attribute which references the iterated variable in the inner component:

    <my-list [data]="cars">
      <template let-item>
        <li>
          <div>{{item.make | uppercase}}</div>
        </li>
      </template>
    </my-list>
    
  3. The inner component has <template> as well and uses a variant of ngFor like so:

    <ul>
        <template #items ngFor [ngForOf]="data" [ngForTemplate]="tmpl">
            -- insert template here --
        </template>
    </ul>
    
  4. The 'tmpl' variable assigned to the ngForTemplate attribute needs to be fetched in the component code:

    export class MyListComponent {
        @Input() data: any[];
        @ContentChild(TemplateRef) tmpl: TemplateRef;
    }
    
  5. @ContentChild and TemplateRef are angular bits, so need to be imported

    import { Component, Input, ContentChild, TemplateRef } from '@angular/core';
    

See the fork of your plunkr with these changes in here plnkr.

This isn't the most satisfactory solution for your stated problem, since you're passing the data in to the list you might as well have the ngFor on the outer. Plus, the additional content (the literal '-- insert template here --') gets dropped, so it you wanted to display that it must be on the outer template as well.

I can see it might be useful where the iteration is provided within the inner component (say from a service call), and maybe to do some manipulation of the template in code.

msanford
  • 11,803
  • 11
  • 66
  • 93
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • thank you so much for this answer and the plunkr. Though this seems to work, in my case when I have a dynamic list nothing is rendered in the list. Though if I inspect the DOM I can see there are template bindings. Any idea as to what's going on? – sohel101091 Mar 09 '17 at 19:06
  • Not sure of the scenario - I can add cars to the list at runtine with a button and click event handler, and they show up in the list. – Richard Matsen Mar 10 '17 at 18:50
  • Spent many hours back in August to find a solution. This answer is soooo helpful. It just works. Thanks a lot!!! – Vildan Oct 18 '17 at 08:39
1
  • <ng-content> inside *ngFor doesn't work, therefore

    <li *ngFor="let item of data">
        -- insert template here --
        <ng-content></ng-content>
    </li>
    

won't do anything meaningful. Everything will be transcluded to the first <ng-content>

https://github.com/angular/angular/issues/8563 might solve some of your requirements.

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    The GitHub issue describes exactly what I need, but it's currently in the state of "Needs Design" with no milestone so that doesn't offer me much. The first link seems exactly what I need, but works with an already outdated version of Angular (beta 8): referencing `TemplateRef` and the call to `setLocal()` don't work anymore. This solution does seem promising though and I saw a comment of yours about the `setLocal()` thing, I'm gonna play around some more with this. – J.P. Jun 27 '16 at 12:15