Reputation: 1055
I have created a reusable component that will render a table. It is currently working for basic tables, but there are scenarios where I would like to be able to customize an individual column/cell within the table.
Here's the code I'm using in the parent component:
<!-- PARENT TEMPLATE -->
<app-table
[headers]="headers"
[keys]="keys"
[rows]="rows">
</app-table>
// PARENT COMPONENT
public headers: string[] = ['First', 'Last', 'Score', 'Date'];
public keys: string[] = ['firstName', 'lastName', 'quizScore', 'quizDate'];
public rows: Quiz[] = [
{ 'John', 'Doe', 0.75, '2020-01-03T18:18:34.549Z' },
{ 'Jane', 'Doe', 0.85, '2020-01-03T18:19:14.893Z' }
];
And the code I'm using in the child component:
<!-- CHILD TEMPLATE -->
<table>
<thead>
<tr>
<td *ngFor="let header of headers">
{{ header }}
</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows">
<td *ngFor="let key of keys">
{{ render(key, row) }}
</td>
</tr>
</tbody>
</table>
// CHILD COMPONENT
@Input() headers: string[];
@Input() keys: string[];
@Input() rows: any[];
render(key: string, row: any) {
return row['key'];
}
I would like to be able to declare a template in my parent component to modify the data in the child component. An example of this would be to convert the quiz score to a percent or to format the date without directly changing the data in the component. I envision something similar to the following:
<ng-template #quizScore>
{{ someReferenceToData | percent }} // This treatment gets passed to child
</ng-template>
And by passing this into my child component, it would take my rendered data and format it using the percent pipe. I've done some research on ngTemplateOutlet
, ngComponentOutlet
, ng-content
, etc. but am unsure of the best approach.
Upvotes: 6
Views: 4723
Reputation: 3312
You'll need to pass TemplateRef
s from your parent component to your child (table) component. Going based on your current approach, one way to do that would be to create another array for @ViewChild
references in the parent component, and pass it to the table component.
However, I would suggest a little refactoring by creating an interface for your column configuration, that has header
,key
, and optionally a customCellTemplate
. Something like:
import { TemplateRef } from '@angular/core';
export interface TableColumnConfig {
header: string;
key: string;
customCellTemplate?: TemplateRef<any>;
}
Then you could simplify the inputs of your child component
// CHILD COMPONENT
@Input() columnConfigs: TableColumnConfig[];
@Input() rows: any[];
Your vision for the custom cell template definition in the parent is on the right track, you'll need to access the data though.
HTML
<app-table
[columnConfigs]="columnConfigs"
[rows]="rows">
</app-table>
<!-- name the $implicit variable 'let-whateverIwant', see below for where we set $implicit -->
<!-- double check variable spelling with what you are trying to interpolate -->
<ng-template #quizScore let-someReferenceToData>
{{ someReferenceToData | percent }} // The parent component can do what it likes to the data and even styles of this cell.
</ng-template>
TS - template references are undefined until ngOnInit
/ ngAfterViewInit
lifecycle hooks
// PARENT COMPONENT
@ViewChild('quizScore', { static: true }) customQuizTemplate: TemplateRef<any>;
public columnConfigs: TableColumnConfiguration[];
public rows: Quiz[] = [
{ 'John', 'Doe', 0.75, '2020-01-03T18:18:34.549Z' },
{ 'Jane', 'Doe', 0.85, '2020-01-03T18:19:14.893Z' }
];
ngOnInit() {
this.columnConfigs = [
{key: 'firstName', header: 'First'},
{key: 'lastName', header: 'Last'},
{key: 'quizScore', header: 'Score', customCellTemplate: customQuizTemplate},
{key: 'quizDate', header: 'Date'}
]
}
Note about static:true
vs static:false
- the template ref is static unless it's dynamically rendered with something like *ngIf
or *ngFor
. static:true
will initialize the template ref in ngOnInit
, static:false
will initialize it in ngAfterViewInit
.
You've already got the nested *ngFor in place in the table, but youll need to do some content projection if there is a customCellTemplate.
<tr *ngFor="let row of rows">
<td *ngFor="let col of columnConfigs">
<!-- if there is no customCellTemplate, just render the data -->
<div *ngIf="!col.customCellTemplate; else customCellTemplate">
{{ render(col.key, row) }}
</div>
<ng-template #customCellTemplate>
<!-- expose data, you could expose entire row if you wanted. but for now this only exposes the cell data -->
<ng-template [ngTemplateOutlet]="col.customCellTemplate"
[ngTemplateOutletContext]="{ $implicit: {{ render(col.key, row) }} }">
</ng-template>
</ng-template>
</td>
</tr>
Sidenote: I would replace render(col.key, row)
with row[col.key]
if you can.
Upvotes: 7
Reputation: 1250
My suggestion here would be to define a column
object like this:
export class Column {
header: string;
key: string;
type: ColumnType; // An enumerator: plain text, number, percentage, date
}
And in your child component you have maybe pipes or other functions that handle the display logic for the different types of columns
render(type: ColumnType, row: any) {
switch(type) {
case ColumnType.Date:
// format as date
break;
case ColumnType.Number:
// Format number to 2 decimals for example
break;
}
}
Upvotes: 1
Reputation: 604
Maybe you can create an event listener in the child component and render the correct data when emitting the formatted data using RxJS.
Upvotes: 1