RichardW
RichardW

Reputation: 969

How to distinguish between multiple templates passed to a component via ContentChildren?

For example, suppose I want to implement a datatable component to which I can pass an optional template for the cells of each column:

@Component({
  selector: 'datatable',
  template: `
<table>
  <tbody>
    <tr *ngFor="let row of rows">
      <td *ngFor="let col of columns">
        <ng-container *ngIf="getTemplate(col)">
          <ng-container *ngTemplateOutlet="getTemplate(col)"
                        ngTemplateOutletContext="{$implicit: row}">
          </ng-container>
        </ng-container>
        <ng-container *ngIf="!col.cellTemplate">
            {{row[col.prop]}}
        </ng-container>
      </td>
    </tr>
  </tbody>
</table>`,
})
export class DatatableComponent implements OnInit {
  @ContentChildren('cellTemplate') cellTemplates: QueryList<TemplateRef<any>>;
  ...
}

Which I want to use as so:

<datatable [columns]='columns'
           [rows]='rowCollection'>
  <ng-template #cellTemplate let-row prop="id">
    <a href='some url'> {{row.id}} </a>
  </ng-template>

  <ng-template #cellTemplate let-row prop="name">
    <a href='some other url'> {{row.name}} </a>
  </ng-template>
</datatable>

The problem I have is: how do I implement the function getTemplate, which should find the correct template in cellTemplates for each column? For example, I have given each template an attribute prop but I don't see how I can access this value from the TemplateRef.

Upvotes: 0

Views: 487

Answers (2)

mikeyc7m
mikeyc7m

Reputation: 93

I also had a fair bit of struggle figuring this out, Angular does a poor job of it. Needing a custom directive to identify each template is a surprising bit of a hack.

So just to expand on the answer from RichardW, the @ContentChildren can be referenced in the DataTable component with a function that finds the right template - where the propName matches a table column name.

showCellTemplate(column = "") {
    return this.cellTemplates.find((obj) => obj.propName === column);
}

In the DataTable component HTML, add a container that is replaced by the template that matched the named column - using *ngTemplateOutlet. Note the template's inner TemplateRef is exposed as .tmpl from the directive's constructor and needs to be referenced (otherwise it throws an error.)

<ng-container *ngIf="showCellTemplate(col) as template">
    <ng-container *ngTemplateOutlet="template.tmpl; 
        context: { $implicit: row }"></ng-container>
</ng-container>

The table row data is sent back to the template as the context and can be accessed with an attribute on the template, e.g. let-row.

<ng-template #cellTemplate="propName" propName="name" let-row>
    <a href='some other url'> {{row.name}} </a>
</ng-template>

Upvotes: 0

RichardW
RichardW

Reputation: 969

I figured this out after a fair bit of struggle. The solution is:

  1. Create a directive that will carry both the identifier for the template and the template itself.
  2. Inject a TemplateRef into the constructor of the directive; when the directive is placed on an ng-template element, the corresponding template is injected.
  3. Set the template reference variable for each template to the directive

Code:

@Directive({selector: '[propName]', 'exportAs': 'propName'})
export class PropName  { 
  constructor (public tmpl: TemplateRef<any>) { }
  @Input('propName') propName: string;    
}

...

<datatable [columns]='columns'
           [rows]='rowCollection'>
  <ng-template #cellTemplate="propName" propName="id">
    <a href='some url'> {{row.id}} </a>
  </ng-template>

  <ng-template #cellTemplate="propName" propName="name">
    <a href='some other url'> {{row.name}} </a>
  </ng-template>
</datatable>

Each element of the ContentChildren list (defined like @ContentChildren('cellTemplate') cellTemplates) will then have both a propName property (identifying the template) and a tmpl property (the TemplateRef).

Upvotes: 2

Related Questions