Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Component transclude with inline template

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.

like image 814
J.P. ten Berge Avatar asked Jun 24 '16 09:06

J.P. ten Berge


1 Answers

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.

like image 193
Richard Matsen Avatar answered Oct 18 '22 19:10

Richard Matsen