使用angular5中的contentChildren获取多个ng-template ref值

时间:2019-12-04 22:08:05

标签: angular angular-material

我正在尝试将多个ng模板传递给我的可重用组件(my-table组件),即内容投影。现在,我需要获取每个传递的ng-template的参考值,以便可以使用该值知道哪个模板传递给哪个列。基本上,我创建了一个可重用的表组件(在Angular材质表的顶部),用户可以为每个列传递一个单独的模板。

请提出建议-还是有更好的方法呢?

temp.component.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>

我可以为每个同伴创建指令,但是列的任何内容都不会更改,因此指令路由将不起作用。我在想,组件用户将传递每个带有模板ref值的列模板作为列标题值,例如,如果用户正在为“ firstName”列传递ng-template,它应该像,

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

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

5 个答案:

答案 0 :(得分:4)

Directive是一种很好的解决方法,因此您已经在朝着正确的方向思考。指令还支持输入参数,因此您可以将列名或标题指定为指令的参数。还要检查official documentation以获得更多详细信息。

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

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 sample,以便您可以在实践中看到它。

答案 1 :(得分:3)

我不得不构建许多使用Angular Material的MatTable的表组件,从长远来看,我决定通过构建一个动态且可重用的基表来节省一些时间。在讨论如何向其添加特定功能之前,我已经添加了一些有关如何启动和运行最小的动态重用表的上下文/思考过程。

构建动态可重用表的建议

我做的第一件事(在将Angular Material添加到项目中之后)是确定我希望消费者如何使用我的表。我决定,任何表级别的行为(启用/禁用分页)都将由表组件中的@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);
  }
}

my-table-component.html

OP在他的回答中显示的相似方法。由于我将customCellTemplate作为属性添加到TableColumnConfig,因此访问它看起来更加干净。还要注意,在本演示中,我决定只将列数据公开给customCellTemplates,但如有必要,您可以通过将$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>

示例:消耗组件

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

app-component.html

对于这个最小的示例,该表只有两个输入。我想在文件底部而不是在表标签本身内部为customCellTemplates定义<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 demo以获得更多代码注释。

答案 2 :(得分:2)

我已经在我的库Easy Angular https://github.com/adriandavidbrand/ngx-ez/tree/master/projects/ngx-ez/src/lib/ez-table

中构建了一个表组件

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

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

该表使用ContentChildren来获取列

@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上。

答案 3 :(得分:2)

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

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>

我为此创建了一个堆叠闪电战。请参阅this

答案 4 :(得分:1)

我在角形材质表组件的顶部创建了以下自定义表组件。

以下是我的业务要求,

  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();
    }
}
  

在parent.component.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;
    }
}