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

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.


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.