Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 each passed ng-template so I can use that value to know, which template is passed for which column. Basically I am creating a reusable table component (on top of Angular material table) where user can pass an separate template for each column.

Kindly suggest - OR is there a better approach of doing this?

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>

I can create directive for each column, but than no of column might change so directive route will not work. I am thinking that, component user will pass each column template with template ref value as column header value, for example, if user is passing an ng-template for "firstName" column, it should be like ,

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

And I need a way to get all the provided ng-template with their ref so I can know, which template belongs to which column.

like image 451
Akash Avatar asked Dec 04 '19 22:12

Akash


Video Answer


2 Answers

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.

like image 180
AlesD Avatar answered Sep 24 '22 13:09

AlesD


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.

like image 38
Anto Antony Avatar answered Sep 21 '22 13:09

Anto Antony