简体   繁体   中英

Display component's content from @ContentChildren in multiple rows using *ngFor on Angular 2

I'm trying to write a simple list view (grid view?) with multiple columns using Angular 2 that I can instantiate this way:

<file-list [items]="items">
    <file-list-column title="Id" field="id"></file-list-column>
    <file-list-column title="Name" field="name"></file-list-column>
</file-list>

I actually managed to make this work in a very hacky way, it generates a table with the information data I set in the FileListColumn component.

This is what I have at the moment:

FileListColumn component, where the column metadata is defined:

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

@Component({
    selector: 'file-list-column',
    template: ''
})
export class FileListColumn {
    @Input()
    public title: string;
    @Input()
    public field: string;

    ngOnInit() {
    }
}

FileList component, this is the main component for the list view:

import {Component, Input, ContentChildren, QueryList, AfterContentInit, forwardRef} from '@angular/core';
import {FileListColumn} from './file.list.column';
import {IItem} from 'models';
@Component({
    selector: 'file-list',
    template: require('./file.list.html')
})
export class FileList implements AfterContentInit {
    @ContentChildren(forwardRef(() => FileListColumn))
    public columns: QueryList<FileListColumn>;

    @Input()
    public items: IItem[];

    constructor() {
    }

    public ngAfterContentInit() {
    }

    public getProperty(item:{[key:string]:any}, name:string) {
        return item[name];
    }
}

This this is the view for the FileList component:

<table>
    <thead>
        <tr>
            <th *ngFor="let col of columns">
                {{col.title}}
            </th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let item of items">
            <td *ngFor="let col of columns">
                {{getProperty(item, col.field)}}
            </td>
        </tr>
    </tbody>
</table>

The hacky thing I mentioned is that, the way it works, for each column of each item, the only thing it does is that it calls the getProperty function passing the item and the field name and returns the value of that field, which is a less than ideal solution.

What I want to be able to do now is to be to set content in my columns the way shown below and have that content shown in my table:

<file-list [items]="items">
    <file-list-column title="Id" field="id"></file-list-column>
    <file-list-column title="Name">
        <span>{{$item.name}}</span>
    </file-list-column>
    <file-list-column title="Some other column">
        <some-component item="$item"></some-component>
    </file-list-column>
</file-list>

Not only that, but I also want to be able to define the html for the column in the FileListColumn component itself in order to keep the code a bit more organized.

One of the things I tried was to add a template in the view of the FileListColumn component:

<template #columnTemplate>
    <span>text to see if worked</span>
    <ng-content></ng-content>
</template>

Then I tried to get its reference in the FileListColumn component with: @ViewChild('columnTemplate') columnTemplate: TemplateRef<any>;

And then I tried to load it in the view of the FileList component by using:

<td *ngFor="let col of columns" *ngForTemplate="col.columnTemplate">
</td>

But I get an error on the browser saying:

Error: Template parse errors:(…) "Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with * ("
    <tbody>
        <tr *ngFor="let item of items">
            <td *ngFor="let col of columns" [ERROR ->]*ngForTemplate="col.columnTemplate">
            </td>
        </tr>
"): FileList@10:44

Even if this worked, I wouldn't have any idea on how to actually bind the item in my items array to the FileListColumn content.

Does anyone have an idea on how I can achieve those things?

I have been looking into a way solve this problem all day but I couldn't find anything useful, most tutorials are irrelevant as they were written for old betas of Angular 2 using older APIs.

Thanks in advance for the help and sorry for the long post.

Update 1:

So, I managed to load the template from FileListColumn inside of the <td> in the FileList using the #columnTemplate attempt by doing this based on one of Günter Zöchbauer's links:

<tr *ngFor="let item of items">
    <td *ngFor="let col of columns">
        <template [ngTemplateOutlet]="col.columnTemplate" [ngOutletContext]="item"></template>
    </td>
</tr>

The template is loaded inside every single cel but there are still two problems :

  • The only properties I can access from the template are the column properties, I have no access to the item properties from the outer ngFor .
  • When I add content to the FileListColumn by doing <file-list-column><div>Content here</div></file-list-column> to be loaded in the <ng-content> of the column's template, this content is only showed in the last row, meaning it's not repeated in all rows.

So, I found the solution for the problem based on the links in Günter Zöchbauer's answer.

In the FileList component I added a <template> element to load the template from my FileListColumn component. It looks like this now:

<tr *ngFor="let item of items">
    <td *ngFor="let col of columns">
        <template [ngTemplateOutlet]="col.columnTemplate" [ngOutletContext]="{ item: item, column: col }"></template>
    </td>
</tr>

As you can see, I pass the item as the ngOutletContext so I can have access to it in the template.

Now, here's what the template of the FileListColumn looks like:

<template #internalColumnTemplate let-item="item">
    <span *ngIf="field">{{item[field]}}</span>
    <template *ngIf="externalTemplate" [ngTemplateOutlet]="externalTemplate" [ngOutletContext]="{ item: item }"></template>
</template>

It has the #internalColumnTemplate selector that I can reference in the code and the let-item="item" which references the item I passed in the ngOutletContext and makes it available inside of the template. So basically, if the field attribute is set, it shows the span with the with the field value otherwise, if externalTemplate is set, which is another property I set in the FileListColumn component, it shows whatever template I pass in the definition of the FileListColumn .

This is the full solution:

FileList component file.list.ts :

import {Component, Input, ContentChildren, QueryList, AfterContentInit, forwardRef} from '@angular/core';
import {FileListColumn} from './file.list.column';
import {IItem} from 'models';

@Component({
    selector: 'file-list',
    template: require('./file.list.html')
})
export class FileList implements AfterContentInit {
    @ContentChildren(forwardRef(() => FileListColumn))
    public columns: QueryList<FileListColumn>;

    @Input()
    public items: IItem[];

    constructor() {
    }

    public ngAfterContentInit() {
    }
}

FileList template file.list.html :

<table>
    <thead>
        <tr>
            <th *ngFor="let col of columns">
                {{col.title}}
            </th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let item of items">
            <td *ngFor="let col of columns">
                <template [ngTemplateOutlet]="col.columnTemplate" [ngOutletContext]="{ item: item }"></template>
            </td>
        </tr>
    </tbody>
</table>

FileListColumn component file.list.component.ts :

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

@Component({
    selector: 'file-list-column',
    template: require('./file.list.column.html')
})
export class FileListColumn {
    @Input()
    public title: string;
    @Input()
    public field: string;

    @ContentChild(TemplateRef)
    public externalTemplate: TemplateRef<any>;

    @ViewChild('internalColumnTemplate') 
    public columnTemplate: TemplateRef<any>;

    ngOnInit() {
    }
}

The externalTemplate property is the optional template that you set when you use the FileList component whereas the columnTemplate is the internal template in the view of the FileListColumn as seen previously and below.

FileListColumn template file.list.column.html :

<template #internalColumnTemplate let-item="item">
    <span *ngIf="field">{{item[field]}}</span>
    <template *ngIf="externalTemplate" [ngTemplateOutlet]="externalTemplate" [ngOutletContext]="{ item: item }"></template>
</template>

Here's how the control FileList component is used after it's setup:

<div>
    <file-list [items]="fakeItems">
        <file-list-column title="Id">
            <template let-item="item">
                <div>external {{item.id}}{{item.name}}</div>
            </template>
        </file-list-column>
        <file-list-column title="Name" field="name"></file-list-column>
    </file-list>
</div>

As you can see, you have the options of just passing the field you want to use and the column will resolve it, or you can set a <template> for the column with whatever html you want to use, and don't forget to add let-item="item" as seen above so you have access to the item.

That's basically it. I hope this is useful for someone else.

If you have any questions or suggestions to improve it, just let me know and I'll update it.

By the way, I'm using Angular 2.0.1 .

You can use ngForTemplate this way:

<template ngFor let-col [ngForOf]="colums" [ngForTemplate]="col?.columnTemplate">
  <td></td>
</template>

I don't know if this solves all your problems though.

This might work as well (not tried myself)

<td *ngFor="let col of columns template:col?.columnTemplate">
</td>

Actually I tried that :-/ Repeating use of ng-content . Perhaps something has changed that breaks that approach.

See also

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM