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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With