Hughes
Hughes

Reputation: 1055

Passing ng-template to child component

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

Answers (3)

Chris Newman
Chris Newman

Reputation: 3312

You'll need to pass TemplateRefs 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:

Refactor

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[];

In Parent Component

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.

In Child Component

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

maury844
maury844

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

Garine
Garine

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

Related Questions