Angular 4: Dynamic template with interpolation

Sergio Girado picture Sergio Girado · Jul 15, 2017 · Viewed 8.3k times · Source

I'm building a data table component that is being designed as very generic component.

The idea is to define the table as this:

<app-datatable [items]="currentPageResult">
    <app-datatable-column attribute="id"
                          header="ID"></app-datatable-column>
    <app-datatable-column attribute="name"
                          header="First Name"></app-datatable-column>
    <app-datatable-column attribute="last_name"
                          header="Last Name"></app-datatable-column>
</app-datatable>

In this way, we can specify the array into the datatable component, and by defining datatable-columns we can decide witch attributes should display the table. Internally the table will do an ngFor by columns and another ngFor by items.

This part was simple and now it's working very well, the thing becomes tricky when I want custom html content into the td; Something like this:

<app-datatable [items]="currentPageResult">
    <app-datatable-column attribute="id"
                          header="ID"
                          [template]="titleTemplate">
        <ng-template #titleTemplate>
            <a role="button" [routerLink]="[data.id]">
                {{data.id}}
            </a>
        </ng-template>
    </app-datatable-column>
    <app-datatable-column attribute="name"
                          header="First Name"></app-datatable-column>
    <app-datatable-column attribute="last_name"
                          header="Last Name"></app-datatable-column>
</app-datatable>

Notice that i'm using data as the iteration variable, but it actually doesn't work, I'm just illustrating what I want to do.

To solve this, I've used a '$data' as asimple string and a custom directive to replace '$data' with the corresponding column/row value.

In the datatable component I'm using a appDatatableContent directive on each tbody td passig the row data and the colum settings (the directive only applies if the colum settings has a template):

<table class="table">
  <thead>
    <tr>
      <th *ngFor="let col of columns" [ngClass]="getColumnHeaderClasses(col)" (click)="onReorder(col)">{{col.header}}</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let row of items.Items">
      <td *ngFor="let col of columns" appDatatableContent [column]="col" [row]="row">
        <ng-container *ngIf="!col.template; else col.template">
          {{row[col.attribute]}}
        </ng-container>
      </td>
    </tr>
  </tbody>
</table>

And the directive basically looks for an element that contains '$data' inside and replaces '$sdata' for the corresponding column value as this:

ngAfterViewInit() {
  if (!this.column.template) {
    return;
  }
  const element: HTMLElement = this.element.nativeElement;
  const value = this.row[this.column.attribute];

  const children = element.getElementsByTagName('*');
  const length = children.length;

  for (let i = 0; i < length; i++) {
    const currentNode = children[i];
    if (!currentNode.children || !currentNode.children.length) {
      const originalHTML: string = currentNode.innerHTML;
      const fixedHTML = originalHTML.replace('$data', value);
      currentNode.innerHTML = fixedHTML;
    }
  }
}

Also, notice that each cell has <ng-container *ngIf="!col.template; else col.template"> so if there is any template binded, the cell content will render that template, but the problem is how to pass arguments (specifically the row object) to the template so the template can use interpolation with that parameter.

See working plunker: https://plnkr.co/edit/84jhiquT5q3OQaCxTa5i

But It doesn't seems to be the best approach, because i can not get advantage of a lot o angular power since I'm just replacing a string value.

So, ¿How can I define a dynamic template that can use a iteration variable to render custom cell contents?

Answer

yurzui picture yurzui · Jul 15, 2017

You can easily solve your problem by using built-in directive NgTemplateOutlet which allows you to pass context to EmbeddedView (ng-template)

First remove DatatableContentDirective

Then change markup in

data-table.component.html

<td *ngFor="let col of columns">
  <ng-container *ngIf="!col.template; else customTemplate">
     {{row[col.attribute]}}
  </ng-container>
  <ng-template #customTemplate 
     [ngTemplateOutlet]="col.template"
     [ngTemplateOutletContext]="{ col: col, row: row }">
  </ng-template>
</td>

and use in parent component

<ng-template #titleTemplate let-col="col" let-row="row">
  <a role="button" ...>
       <b>Custom</b> {{row[col.attribute]}}
  </a>
</ng-template>

Plunker Example

See also more information about template variable let-name