Get multiple ng-template ref values using contentChildren in angular 5

前端 未结 5 2464
孤城傲影
孤城傲影 2021-02-20 15:11

I am trying to pass multiple ng-template to my reusable component (my-table component), content projection. Now I need to get the reference value of ea

相关标签:
5条回答
  • 2021-02-20 15:55

    I've had to build lots of table components that used Angular Material's MatTable, and at some point I decided to save myself some time in the long run by building a base table that is dynamic and reusable. I've added a bit more context / thought process around how to get up and running with a bare minimum dynamic reusable table, before talking about how to add a specific feature to it.

    Advice for Building a Dynamic and Reusable Table

    The first thing I did (after adding Angular Material to the project) was determine how I want consumers to use my table. I decided that any table level behavior (enable/disable pagination) would be controlled by @Input's in the table component. However as I developed it further, I realized most of the new functionality I needed should really be controlled per-column. The rest of this answer is focused on the per-column configuration.

    TableColumnConfig Interface - adding a new feature

    I started off by defining an interface for a configuration object (just like OP did with TableColumns except mine is called TableColumnConfig. The bare minimum needed for dynamic and reusable functionality are two strings that you use to access the data in each row and to display the column name (I use key and displayName).

    If we want to add the ability for consumers of the component to pass in a custom cell template, I'd first add a property to the TableColumnConfig interface like so:

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

    my-table-component.ts

    I believe I started with the Angular Material schematic for generating a table component, but I didn't like the amount of boilerplate for something bare minimum like this example (it's easy enough to add pagination and sorting later).

    You don't need to do anything special in the table-component.ts for custom the custom cell template functionality (just note we are expecting TableColumnConfig[] from the consuming component), but showing the code below for completeness. Most of the times when I needed to add a per-column feature, I never even had to mess with this file.

    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

    Similar approach to what the OP showed in his answer. Since I added customCellTemplate as a property to TableColumnConfig, accessing it looks a bit cleaner. Also just a note that for this demo I decided to only expose column data to customCellTemplates, but you could easily return the entire row if necessary by changing $implicit: row[col.key] to $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>
    

    Example: Consuming Component

    Sample use case where we want styled text in a column

    app-component.html

    For this bare minimum example, the table only has two inputs. I like to define the <ng-template>s for customCellTemplates at the bottom of the file instead of inside of the table tag itself for better readability 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
          }
        ];
      }
    }
    

    Check out my StackBlitz demo for a few more code comments.

    0 讨论(0)
  • 2021-02-20 15:57

    A Directive is a good approach for this so you are already thinking in the right direction. Directives support also input parameters so you can specify the column name or header as the parameter to the directive. Check also the official documentation for more details.

    Here is a sample directive using this approach:

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

    As you can see the directive has an input property that will receive the column name and also it injects the TemplateRef so you can access it directly from the directive.

    You can then define the columns like this:

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

    In the component you then query the ContentChildren by the directive and get all the directives which gives you access to the column names and templates.

    Here is the updated component:

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

    Here is a slightly different way to do it maybe you like this more. I will now base it on your custom table sample since you provided more information.

    You can create a directive that takes content and you specify the template as the content. Here is a sample implementation:

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

    Then your parent component template will change to this:

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

    Your custom table component needs to be changed. instead of receiving the templateNameList it needs to generate it from the ContentChildren on demand.

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

    If you don't like this second approach you can still use what I suggested in the original sample in the same way. The only difference is how it looks in the template. I created also a StackBlitz sample so you can see it in practice.

    0 讨论(0)
  • 2021-02-20 16:00

    I have built a table component in my library Easy Angular https://github.com/adriandavidbrand/ngx-ez/tree/master/projects/ngx-ez/src/lib/ez-table

    Each column can take a template via a ViewChild

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

    The table uses ContentChildren to get the columns

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

    and the table component passes the current item in with a context when rendering

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

    and is used like

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

    Here is a demo of how it works

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

    There is quite a bit to this table but all the source is up on GitHub.

    0 讨论(0)
  • 2021-02-20 16:06

    There is another approach for creating the custom table component. Instead of exposing just the columns, you can have the access to the entire rows. So you can have the direct control over the entire columns.

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

    Now you can provide the details as follows,

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

    I have created a stackblitz for the same. Please refer this.

    0 讨论(0)
  • 2021-02-20 16:06

    I have created the below custom table component, on top of angular material table component.

    Following were my business requirement,

    1. every cell can have multiple component OR plain text OR image.
    2. table should be sortable
    3. column might not have header values(empty header) but can have cell content.

    So I need to have the full control on each cell template and the events raised by any element from within the cell.

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

    in the 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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题