Отображать содержимое компонента из @ContentChildren в несколько строк, используя *ngFor на Angular 2

Я пытаюсь написать простое представление списка (представление сетки?) с несколькими столбцами, используя Angular 2, которое я могу создать следующим образом:

<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>

На самом деле мне удалось заставить это работать очень хакерским способом, он генерирует таблицу с информационными данными, которые я установил в компоненте FileListColumn.

Вот что у меня есть на данный момент:

Компонент FileListColumn, в котором определяются метаданные столбца:

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, это основной компонент представления списка:

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];
    }
}

Это это представление для компонента FileList:

<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>

Хакерская вещь, которую я упомянул, заключается в том, что, как это работает, для каждого столбца каждого элемента единственное, что он делает, это вызывает функцию getProperty, передавая элемент и имя поля, и возвращает значение этого поля, которое является далеко не идеальное решение.

Что я хочу сделать сейчас, так это установить содержимое в моих столбцах, как показано ниже, и отобразить это содержимое в моей таблице:

<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>

Не только это, но я также хочу иметь возможность определять html для столбца в самом компоненте FileListColumn, чтобы код был немного более организованным.

Одна из вещей, которые я пробовал, заключалась в том, чтобы добавить шаблон в представление компонента FileListColumn:

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

Затем я попытался получить его ссылку в компоненте FileListColumn с помощью: @ViewChild('columnTemplate') columnTemplate: TemplateRef<any>;

И затем я попытался загрузить его в представлении компонента FileList, используя:

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

Но я получаю сообщение об ошибке в браузере:

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

Даже если бы это сработало, я бы понятия не имел, как на самом деле связать элемент в моем массиве элементов с содержимым FileListColumn.

У кого-нибудь есть идеи о том, как я могу достичь этих вещей?

Я искал способ решить эту проблему весь день, но не смог найти ничего полезного, большинство руководств не имеют значения, поскольку они были написаны для старых бета-версий Angular 2 с использованием более старых API.

Заранее спасибо за помощь и извините за длинный пост.

Обновление 1:

Итак, мне удалось загрузить шаблон из FileListColumn внутри <td> в FileList, используя попытку #columnTemplate, сделав это на основе одной из ссылок Гюнтера Цохбауэра:

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

Шаблон загружается внутри каждой ячейки, но есть две проблемы:

  • Единственные свойства, к которым я могу получить доступ из шаблона, — это свойства столбца, у меня нет доступа к свойствам элемента из внешнего ngFor.
  • Когда я добавляю содержимое в FileListColumn, выполняя <file-list-column><div>Content here</div></file-list-column> для загрузки в <ng-content> шаблона столбца, это содержимое отображается только в последней строке, то есть оно не повторяется во всех строках.

person Eric.M    schedule 04.10.2016    source источник


Ответы (2)


Итак, я нашел решение проблемы на основе ссылок в ответе Гюнтера Цохбауэра.

В компоненте FileList я добавил элемент <template> для загрузки шаблона из моего компонента FileListColumn. Теперь это выглядит так:

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

Как видите, я передаю item как ngOutletContext, чтобы иметь доступ к нему в шаблоне.

А вот как выглядит шаблон FileListColumn:

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

У него есть селектор #internalColumnTemplate, на который я могу ссылаться в коде, и let-item="item", который ссылается на элемент, который я передал в ngOutletContext, и делает его доступным внутри шаблона. Таким образом, в основном, если установлен атрибут field, он показывает span со значением поля, в противном случае, если установлено externalTemplate, что является еще одним свойством, которое я установил в компоненте FileListColumn, он показывает любой шаблон, который я передаю в определении FileListColumn .

Это полное решение:

Компонент FileList 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 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 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() {
    }
}

Свойство externalTemplate — это необязательный шаблон, который вы устанавливаете при использовании компонента FileList, тогда как columnTemplate — это внутренний шаблон в представлении FileListColumn, как показано ранее и ниже.

Шаблон FileListColumn 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>

Вот как компонент управления FileList используется после его настройки:

<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>

Как видите, у вас есть возможность просто передать поле, которое вы хотите использовать, и столбец разрешит его, или вы можете установить <template> для столбца с любым HTML-кодом, который вы хотите использовать, и не забудьте добавить let-item="item" как показано выше, чтобы у вас был доступ к элементу.

Это в основном все. Я надеюсь, что это полезно для кого-то еще.

Если у вас есть какие-либо вопросы или предложения по его улучшению, просто дайте мне знать, и я обновлю его.

Кстати, я использую Angular 2.0.1.

person Eric.M    schedule 05.10.2016

Вы можете использовать ngForTemplate таким образом:

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

Я не знаю, решит ли это все ваши проблемы.

Это тоже может сработать (сам не пробовал)

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

На самом деле я пробовал это :-/ Повторное использование ng-content. Возможно, что-то изменилось, что ломает этот подход.

Смотрите также

person Günter Zöchbauer    schedule 04.10.2016
comment
Хорошо, первый выдает мне Cannot read property 'columnTemplate' of undefined, если я уберу col. из ngForTemplate, я перестану получать ошибку, но ничего не загружается. Второй выдает ту же ошибку, что и первый, что странно, потому что я проверил, и у columnTemplate есть шаблон, и, если я удалю шаблон и поставлю {{col.field}} в ‹td›, будет показан текст поля. - person Eric.M; 05.10.2016
comment
Кстати, эти попытки были внутри шаблона компонента FileList, для моего варианта использования не имеет смысла вызывать тег ‹template› в качестве содержимого для FileList, как в ‹file-list›‹template ngFor (.. .)›‹/список-файлов›. Думаю, если бы я имел дело только с шаблонами для строк, это сработало бы, но здесь я пытаюсь создать шаблоны для столбцов, которые повторяются для каждой отдельной строки. - person Eric.M; 05.10.2016
comment
Я думаю, вам нужен дополнительный ?. Смотрите мой обновленный ответ. - person Günter Zöchbauer; 05.10.2016
comment
Не совсем, я на самом деле нашел решение проблемы, я опубликую его как ответ на свой вопрос, он основан на двух ссылках, которые вы прислали. Огромное спасибо за помощь! - person Eric.M; 05.10.2016