使用 contentChild 在角度 5 中获取多个 ng 模板引用值



我正在尝试将多个ng-template传递给我的可重用component(我的表组件),内容投影。现在我需要获取每个传递ng-template的引用值,以便我可以使用该值来了解为哪个列传递了哪个模板。基本上,我正在创建一个可重用的表格组件(在 Angular 材料表的顶部),用户可以在其中为每列传递一个单独的模板。

请建议 - 或者有更好的方法来做到这一点?

临时组件.ts

import { Component, OnInit, ContentChildren, QueryList, TemplateRef, AfterContentInit } from '@angular/core';
@Component({
selector: 'my-table',
template: `<h1>This is the temp component</h1>`,
styleUrls: ['./temp.component.scss']
})
export class TempComponent implements OnInit, AfterContentInit {
constructor() { }
@ContentChildren(TemplateRef) tempList: QueryList<TemplateRef<any>>;
ngOnInit() {
}
ngAfterContentInit() {
console.log('template list');
console.log(this.tempList);
}
}

app.component.html

<my-table>
<ng-template #column1 let-company let-func="func">
<h1>this template is for column 1</h1>
</ng-template>
<ng-template #column2 let-company let-func="func">
<h1>this template is for column 2</h1>
</ng-template>
</my-table>

我可以为每列创建指令,但不会改变任何列,因此指令路由将不起作用。我在想,组件用户将使用模板引用值作为列标题值传递每个列模板,例如,如果用户传递"firstName"列的ng-template,它应该像,

<ng-template #firstName let-firstname>
<h1>this template is for column firstName</h1>
</ng-template> 

我需要一种方法来获取所有提供的ng-template及其 ref,以便我知道哪个模板属于哪一列。

Directive

是一个很好的方法,所以你已经在朝着正确的方向思考了。指令还支持输入参数,因此您可以将列名或标题指定为指令的参数。另请查看官方文档以获取更多详细信息。

下面是使用此方法的示例指令:

import { Directive, TemplateRef, Input } from '@angular/core';
@Directive({
selector: '[tableColumn]'
})
export class TableColumnDirective {
constructor(public readonly template: TemplateRef<any>) { }
@Input('tableColumn') columnName: string;
}

如您所见,该指令具有一个将接收列名的输入属性,并且它还注入了TemplateRef,因此您可以直接从指令访问它。

然后,您可以像这样定义列:

<ng-template tableColumn="firstname" let-firstname>
<h1>this template is for column firstName</h1>
</ng-template>
<ng-template tableColumn="lastName" let-lastname>
<h1>this template is for column lastName</h1>
</ng-template>

在组件中,然后通过指令查询ContentChildren并获取所有指令,从而可以访问列名和模板。

以下是更新的组件:

import { Component, OnInit, ContentChildren, QueryList, TemplateRef, AfterContentInit } from '@angular/core';

@Component({
selector: 'my-table',
template: `<h1>This is the temp component</h1>`,
styleUrls: ['./temp.component.scss']
})
export class TempComponent implements OnInit,AfterContentInit {
constructor() { }
@ContentChildren(TableColumnDirective) columnList: QueryList<TableColumnDirective>;
ngOnInit() {
}
ngAfterContentInit(){
console.log('column template list');
console.log(this.columnList.toArray());
}
}

这里有一种稍微不同的方法,也许你更喜欢这个。我现在将它基于您的自定义表示例,因为您提供了更多信息。

您可以创建一个获取内容的指令,并将模板指定为内容。下面是一个示例实现:

@Directive({
selector: 'custom-mat-column',
})
export class CustomMatColumnComponent {
@Input() public columnName: string;
@ContentChild(TemplateRef) public columnTemplate: TemplateRef<any>;
}

然后,您的父组件模板将更改为以下内容:

<custom-mat-table [tableColumns]="columnList" [tableDataList]="tableDataList 
(cellClicked)="selectTableData($event)" (onSort)="onTableSort($event)" class="css-class-admin-users-table">
<custom-mat-column columnName="firstname">
<ng-template let-item let-func="func">
<div class="css-class-table-apps-name">
<comp-avatar [image]="" [name]="item?.processedName" [size]="'small'"></comp-avatar>
<comp-button (onClick)="func(item)" type="text">{{item?.processedName}}</comp-button>
</div>
</ng-template>
</custom-mat-column>
<custom-mat-column columnName="status">
<ng-template #status let-item>
<div [ngClass]="{'item-active' : item?.status, 'item-inactive' : !item?.status}"
class="css-class-table-apps-name">{{item?.status | TextCaseConverter}}
</div>
</ng-template>
</custom-mat-column>
<custom-mat-column columnName="lastname">
<ng-template #lastname let-item>
<div class="css-class-table-apps-name">
{{item?.lastname}}</div>
</ng-template>
</custom-mat-column>
</custom-mat-table>

您的自定义表组件需要更改。 它不是接收templateNameList而是需要按需从ContentChildren生成它。

@Component({
selector: 'custom-mat-table',
templateUrl: './customTable.component.html',
styleUrls: ['./customTable.component.scss']
})
export class NgMatTableComponent<T> implements OnChanges, AfterViewInit {
@ContentChildren(CustomMatColumnComponent) columnDefinitions: QueryList<CustomMatColumnComponent>;
templateNameList: { [key: string]: TemplateRef<any> } {
if (this.columnDefinitions != null) {
const columnTemplates: { [key: string]: TemplateRef<any> } = {};
for (const columnDefinition of this.columnDefinitions.toArray()) {
columnTemplates[columnDefinition.columnName] = columnDefinition.columnTemplate;
}
return columnTemplates;
} else {
return {};
}
};
@Input() tableColumns: TableColumns[] = [];
@Input() tableDataList: T[] = [];
@Output() cellClicked: EventEmitter<PayloadType> = new EventEmitter();
@Output() onSort: EventEmitter<TableSortEventData> = new EventEmitter();
displayedColumns: string[] = [];
tableDataSource: TableDataSource<T>;
@ViewChild(MatSort) sort: MatSort;
constructor() {
this.tableDataSource = new TableDataSource<T>();
}
onCellClick(e: T, options?: any) {
this.cellClicked.emit({ 'row': e, 'options': options });
}
ngOnChanges(change: SimpleChanges) {
if (change['tableDataList']) {
this.tableDataSource.emitTableData(this.tableDataList);
this.displayedColumns = this.tableColumns.map(x => x.displayCol);
}
}
ngAfterViewInit() {
this.tableDataSource.sort = this.sort;
}
sortTable(e: any) {
const { active: sortColumn, direction: sortOrder } = e;
this.onSort.emit({ sortColumn, sortOrder });
}
}

如果您不喜欢第二种方法,您仍然可以以相同的方式使用我在原始示例中建议的内容。唯一的区别是它在模板中的外观。 我还创建了一个 StackBlitz 示例,以便您可以在实践中看到它。

我不得不构建许多使用 Angular MaterialMatTable的表组件,在某些时候,我决定通过构建一个动态且可重用的基表来节省一些时间。在讨论如何向其添加特定功能之前,我围绕如何启动和运行一个最低限度的动态可重用表添加了更多的上下文/思考过程。

构建动态且可重用表的建议

我做的第一件事(在将 Angular 材质添加到项目中之后)是确定我希望消费者如何使用我的表格。我决定任何表级行为(启用/禁用分页)都将由表组件中的@Input控制。然而,随着我的进一步发展,我意识到我需要的大多数新功能实际上应该按列进行控制。本答案的其余部分侧重于每列配置。

TableColumnConfig界面 - 添加新功能

我首先为配置对象定义了一个接口(就像 OP 对TableColumns所做的那样,除了我的被称为TableColumnConfig.动态和可重用功能所需的最低限度是两个字符串,用于访问每行中的数据并显示列名(我使用keydisplayName)。

如果我们想添加组件的使用者传入自定义单元格模板的功能,我将首先向TableColumnConfig接口添加一个属性,如下所示:

import { TemplateRef } from '@angular/core';
export interface TableColumnConfig {
displayName: string;
key: string;
customCellTemplate?: TemplateRef<any>; // custom cell template!
}

my-table-component.ts

我相信我从生成表组件的 Angular Material 原理图开始,但我不喜欢像此示例这样最低限度的样板数量(稍后添加分页和排序很容易)。

您无需在 table-component.ts 中执行任何特殊操作即可自定义自定义单元格模板功能(请注意,我们期望使用组件提供TableColumnConfig[]),但为了完整性,请显示下面的代码。大多数时候,当我需要添加每列功能时,我什至不必弄乱这个文件。

import { Component, OnInit, Input } from '@angular/core';
import { MatTableDataSource } from '@angular/material';
import { TableColumnConfig } from './table-column-config';
@Component({
selector: 'app-my-table',
templateUrl: './my-table.component.html',
styleUrls: ['./my-table.component.css']
})
export class MyTableComponent implements OnInit {
@Input() data: any[];
@Input() columnConfigs: TableColumnConfig[];
dataSource: MatTableDataSource<any>;
// need a string array for *matHeaderRowDef and *matRowDef
displayedColumns: string[];
ngOnInit() {
this.displayedColumns = this.columnConfigs.map(config => config.key);
this.dataSource = new MatTableDataSource(this.data);
}
}

我的表组件.html

与OP在他的回答中显示的方法类似。由于我添加了customCellTemplate作为属性TableColumnConfig,因此访问它看起来更干净一些。另外请注意,对于此演示,我决定仅向自定义单元格模板公开列数据,但如有必要,您可以通过将$implicit: row[col.key]更改为$implicit: row来轻松返回整行

<div class="mat-elevation-z8">
<mat-table class="full-width-table" [dataSource]="dataSource">
<!-- NgFor Columns -->
<ng-container *ngFor="let col of columnConfigs" matColumnDef="{{ col.key }}">
<mat-header-cell *matHeaderCellDef> {{ col.displayName }}
</mat-header-cell>
<mat-cell *matCellDef="let row">
<!-- handle custom cell templates -->
<div *ngIf="!col.customCellTemplate; else customCellTemplate">
{{ row[col.key] }}
</div>
<ng-template #customCellTemplate>
<!-- for now, only exposing row[col.key] instead of entire row -->
<ng-template [ngTemplateOutlet]="col.customCellTemplate"
[ngTemplateOutletContext]="{ $implicit: row[col.key] }">
</ng-template>
</ng-template>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</div>

示例:使用组件

我们希望在列中设置样式文本的示例用例

应用组件.html

对于此最低限度的示例,该表只有两个输入。我喜欢在文件底部而不是表标签本身内部定义自定义单元格模板的<ng-template>,以获得更好的可读性 imo。

<app-my-table [data]="tableData" [columnConfigs]="columnConfigs">
</app-my-table>
<!-- Custom cell template for color column -->
<!-- name the $implicit variable 'let-whateverIwant' -->
<ng-template #customCell let-colorData>
<span [ngStyle]="{'color': colorData}">{{colorData}}</span>
</ng-template>

app-component.ts

export class AppComponent implements OnInit {
@ViewChild("customCell", { static: true })
customCell: TemplateRef<any>;
columnConfigs: TableColumnConfig[];
tableData = [
{ id: 1, name: "Chris", color: "#FF9900" },
{ id: 2, name: "Akash", color: "blue" }
];
// we can't reference our {static:true} TemplateRef until ngOnInit
ngOnInit() {
this.columnConfigs = [
{ key: "id", displayName: "ID" },
{ key: "name", displayName: "Name" },
{
key: "color",
displayName: "Favorite Color",
customCellTemplate: this.customCell
}
];
}
}

查看我的 StackBlitz 演示以获取更多代码注释。

还有另一种创建自定义表组件的方法。您可以访问整行,而不是仅公开列。因此,您可以直接控制整个列。

custom-table.component.html

<table>
<!-- Caption -->
<ng-container *ngTemplateOutlet="captionTemplate ? captionTemplate: defaultCaption; context:{$implicit: caption}">
</ng-container>
<!-- Header -->
<thead>
<ng-container *ngTemplateOutlet="headerTemplate ? headerTemplate: defaultHeader; context:{$implicit: columns}">
</ng-container>
</thead>
<!-- Body -->
<tbody>
<!-- Here we will provide custom row Template -->
<ng-template ngFor let-rowData let-rowIndex="index" [ngForOf]="values">
<ng-container
*ngTemplateOutlet="bodyTemplate ? bodyTemplate: defaultBody; context:{$implicit: rowData,columns: columns , index:rowIndex }">
</ng-container>
</ng-template>
</tbody>
<!-- Footer -->
<tfoot>
<ng-template ngFor let-rowData let-rowIndex="index" [ngForOf]="footerValues">
<ng-container
*ngTemplateOutlet="footerTemplate ? footerTemplate: defaultFooter; context:{$implicit: rowData,columns: columns , index:rowIndex }">
</ng-container>
</ng-template>
</tfoot>
</table>
<!-- Caption Default Template -->
<ng-template #defaultCaptio let-caption>
<caption *ngIf="caption">{{caption}}</caption>
</ng-template>
<!-- Header Default Template -->
<ng-template #defaultHeader let-columns>
<tr>
<th *ngFor="let column of columns">{{column.title}}</th>
</tr>
</ng-template>
<!-- Body Default Template -->
<ng-template #defaultBody let-item let-columns="columns">
<tr>
<td *ngFor="let column of columns">{{item[column.key]}}</td>
</tr>
</ng-template>
<!-- Footer Default Template -->
<ng-template #defaultFooter>
<tr *ngFor="let item of footerValues">
<td *ngFor="let column of columns">{{item[column.key]}}</td>
</tr>
</ng-template>

custom-table.component.ts

import {
Component,
OnInit,
Input,
TemplateRef,
ContentChild
} from "@angular/core";
@Component({
selector: "app-custom-table",
templateUrl: "./custom-table.component.html",
styleUrls: ["./custom-table.component.css"]
})
export class CustomTableComponent implements OnInit {
@Input()
caption: string;
@Input()
columns: { title: string; key: string }[] = [];
@Input()
values: any[] = [];
@Input()
footerValues: any[] = [];
@ContentChild("caption", { static: false })
captionTemplate: TemplateRef<any>;
@ContentChild("header", { static: false })
headerTemplate: TemplateRef<any>;
@ContentChild("body", { static: false })
bodyTemplate: TemplateRef<any>;
@ContentChild("footer", { static: false })
footerTemplate: TemplateRef<any>;
constructor() {}
ngOnInit() {}
}

现在,您可以提供详细信息,如下所示,

<app-custom-table [columns]="columns" [values]="values" [footerValues]="footerValues">
<!-- Caption Custom Template -->
<ng-template #caption>
<caption>Custom Table</caption>
</ng-template>
<!-- Header Custom Template -->
<ng-template #header let-columns>
<tr>
<th *ngFor="let column of columns">[{{column.title}}]</th>
</tr>
</ng-template>
<!-- Body Custom Template -->
<ng-template #body let-item let-columns="columns">
<tr *ngIf="item.id === 1 else diff">
<td *ngFor="let column of columns">
<span *ngIf="column.title === 'Name'" style="background-color: green">{{item[column.key]}}</span>
<span *ngIf="column.title !== 'Name'">{{item[column.key]}}</span>
</td>
</tr>
<ng-template #diff>
<tr style="background-color: red">
<td *ngFor="let column of columns">{{item[column.key]}}</td>
</tr>
</ng-template>
</ng-template>
<!-- Footer Custom Template -->
<ng-template #footer let-item let-columns="columns">
<tr>
<td [colSpan]="columns.length">{{item.copyrightDetails}}</td>
</tr>
</ng-template>
</app-custom-table>

我已经为同样的东西创建了一个堆栈闪电战。请参考这里。

我已经在我的库中构建了一个表格组件 简易角度 https://github.com/adriandavidbrand/ngx-ez/tree/master/projects/ngx-ez/src/lib/ez-table

每列都可以通过 ViewChild 获取一个模板

@ContentChild(TemplateRef)
template: TemplateRef<any>;

该表使用 ContentChild 获取列

@ContentChildren(EzColumnComponent)
columns: QueryList<EzColumnComponent>;

并且表组件在呈现时将当前项与上下文一起传递

<ng-container *ngTemplateOutlet="column.template || defaultColumTemplate;context:{ $implicit: item, index: i }"></ng-container>

并且像

<ez-table [data]="data">
<ez-column heading="Heading" property="prop">
<ng-template let-item>
Use item view variable in template here
</ng-template>
</ez-column>
<ez-table>

这是它如何工作的演示

https://stackblitz.com/edit/angular-npn1p1

这张表有很多内容,但所有源代码都在 GitHub 上。

我在角度材料表组件之上创建了以下自定义表组件。

以下是我的业务需求,

  1. 每个单元格可以有多个组件或纯文本或图像。
  2. 表格应该是可排序的
  3. 列可能没有标题值(空标题),但可以包含单元格内容。

因此,我需要完全控制每个单元格模板以及单元格内任何元素引发的事件。

customTable.component.html

<div class="mat-elevation-z8 css-class-table">
<mat-table #table [dataSource]="tableDataSource" matSort (matSortChange)="sortTable($event)">
<ng-container *ngFor="let col of tableColumns; let colIndex=index" matColumnDef="{{col?.displayCol}}">
<mat-header-cell *matHeaderCellDef mat-sort-header class="css-class-table-header css-class-table-header-visibility">
{{col?.headerCol}}
</mat-header-cell>
<mat-cell *matCellDef="let row; let i=index" >
<ng-container [ngTemplateOutlet]="templateNameList[col?.displayCol] || noTemplate"
[ngTemplateOutletContext]="{$implicit:row,func:onCellClick.bind(this)}">
</ng-container>
<ng-template #noTemplate>
{{row[col.displayCol]}}
</ng-template>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; let i=index"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns; let i=index" class="css-class-grid-row"></mat-row>
</mat-table>
</div>

customTable.component.ts

import { Component, Input, ViewChild, AfterViewInit, OnChanges, Output, EventEmitter, TemplateRef, SimpleChanges, ContentChild, ContentChildren } from '@angular/core';
import { MatTableDataSource, MatSort, MatPaginator } from '@angular/material';
import { DataSource } from '@angular/cdk/table';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
export interface TableColumns {
displayCol: string;
headerCol: string;
}
export interface TableSortEventData {
sortColumn: string;
sortOrder: string;
}
export interface PayloadType {
row: any;
options?: any;
}

@Component({
selector: 'custom-mat-table',
templateUrl: './customTable.component.html',
styleUrls: ['./customTable.component.scss']
})
export class NgMatTableComponent<T> implements OnChanges, AfterViewInit {
@Input() templateNameList: Object;
@Input() tableColumns: TableColumns[] = [];
@Input() tableDataList: T[] = [];
@Output() cellClicked: EventEmitter<PayloadType> = new EventEmitter();
@Output() onSort: EventEmitter<TableSortEventData> = new EventEmitter();
displayedColumns: string[] = [];
tableDataSource: TableDataSource<T>;
@ViewChild(MatSort) sort: MatSort;
constructor() {
this.tableDataSource = new TableDataSource<T>();
}
onCellClick(e: T, options?: any) {
this.cellClicked.emit({ 'row': e, 'options': options });
}
ngOnChanges(change: SimpleChanges) {
if (change['tableDataList']) {
this.tableDataSource.emitTableData(this.tableDataList);
this.displayedColumns = this.tableColumns.map(x => x.displayCol);
}
}
ngAfterViewInit() {
this.tableDataSource.sort = this.sort;
}
sortTable(e: any) {
const { active: sortColumn, direction: sortOrder } = e;
this.onSort.emit({ sortColumn, sortOrder });
}
}
export class TableDataSource<T> extends DataSource<T> {
tableDataSubject = new BehaviorSubject<T[]>([]);
sort: MatSort | null;
private _sort;
constructor() {
super();
}
emitTableData(data: T[]) {
this.tableDataSubject.next(data);
}
connect(): Observable<T[]> {
return this.tableDataSubject.asObservable();
}
disconnect() {
this.tableDataSubject.complete();
}
}

在父组件中.html

<custom-mat-table [tableColumns]="columnList" [tableDataList]="tableDataList"
[templateNameList]="{'firstname':firstname,'lastname':lastname,'status':status}"
(cellClicked)="selectTableData($event)" (onSort)="onTableSort($event)" class="css-class-admin-users-table">
<ng-template #firstname let-item let-func="func">
<div class="css-class-table-apps-name">
<comp-avatar [image]="" [name]="item?.processedName" [size]="'small'"></comp-avatar>
<comp-button (onClick)="func(item)" type="text">{{item?.processedName}}</comp-button>
</div>
</ng-template>
<ng-template #status let-item>
<div [ngClass]="{'item-active' : item?.status, 'item-inactive' : !item?.status}"
class="css-class-table-apps-name">{{item?.status | TextCaseConverter}}
</div>
</ng-template>
<ng-template #lastname let-item>
<div class="css-class-table-apps-name">
{{item?.lastname}}</div>
</ng-template>
</custom-mat-table>

parent.component.ts

columnList: TableColumns[] = [
{ displayCol: 'firstname', headerCol: 'First Name' },
{ displayCol: 'lastname', headerCol: 'Last Name' },
{ displayCol: 'status', headerCol: 'Status' }
];
templateList: Object = "{'firstname':firstname,'lastname':lastname,'status':status}";
onTableSort(e: TableSortEventData) {
this.sortQueryParam = {};
if (e && e.sortOrder !== '') {
this.sortQueryParam['sortBy'] = e.sortColumn;
this.sortQueryParam['order'] = e.sortOrder.toUpperCase();
}
else {
this.sortQueryParam = null;
}
}

最新更新