Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass data through nested ng-containers & ng-templates?

I am creating a datatable module and I am aiming for a particular implementation. I want to be able to import the module, and use it's components like so:

randomcomponent.component.html

    <datatable [data]="tableData">
      <datatable-column>
        <ng-template let-row="row">
          <label> {{ row.value }} </label>
        </ng-template>
      </datatable-column>
    </datatable>

Here are the components from the datatable module:

datatable.component.html (<datatable>)

<table class="datatable">
  <thead>
    <tr>
      <th *ngFor="let header of tableData?.headers">
        {{ header?.title }}
      </th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let row of tableData?.data">
       <ng-container *ngTemplateOutlet="template; context: { row: row }"></ng-container>
    </tr>
  </tbody>
  <tfoot>
  </tfoot>
</table>

column.component.html (<datatable-column>)

<ng-template let-row="row">
  <ng-container *ngTemplateOutlet="template; context: { row: row }"></ng-container>
</ng-template>

The data isn't rendering though. I had used a previous approach with ng-content but was not able to loop through content. Only 1 displayed. How can I end up with the implementation I am looking for? Should I seek a different approach?

UPDATE: Provided is my StackBlitz https://stackblitz.com/edit/angular-4esuor

like image 312
francojay Avatar asked Apr 30 '20 14:04

francojay


People also ask

Can you nest ng containers?

in this case you can use ng-container , you will write a nested code, but the output will not be nested. From the Angular doc ng-container: The Angular <ng-container> is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM.

Can we use ng-container inside ng-template?

To sum up, ng-content is used to display children in a template, ng-container is used as a non-rendered container to avoid having to add a span or a div, and ng-template allows you to group some content that is not rendered directly but can be used in other places of your template or you code.

Can we add ngIf to ng-template?

ng-template with ngIf, then & else We use the template reference variable to get the reference to those blocks. In the *ngIf condition we specify the template to render by pointing to the template variable to the then & else condition. The above ngif can be written using the ng-template syntax.

What is the use of NG-template?

One of the main uses for <ng-template> is to hold template content that will be used by Structural directives. Those directives can add and remove copies of the template content based on their own logic. When using the structural directive shorthand, Angular creates an <ng-template> element behind the scenes.


1 Answers

The key here is that you need to find the nested template inside the outer templates, and then iterate those outer templates.

You could enlist the help of another directive here (recommended but not mandatory, see NOTE below), let's call it the datatable-cell directive, it's very simple, declare and export it:

@Directive({
  selector: '[datatable-cell]'
})
export class DatatableCellDirective {
  constructor(public templateRef: TemplateRef<any>) { }
}

Just exposes a template ref, does nothing else.

Next, if using the directive, you add it inside all your columns to the templates (and just declare let-row if using $implicit context... or stick to let-row="row" if you want explicit context):

<datatable-column>
  <ng-template datatable-cell let-row>
    <label> {{ row.value }} </label>
  </ng-template>
</datatable-column>

In your column component, make use of that directive to find and expose the cell template with ContentChild:

@ContentChild(DatatableCellDirective)
cell: DatatableCellDirective

NOTE: You could do this without the DatatableCellDirective, and just set @ContentChild(TemplateRef) cell: TemplateRef<any> in the column, but this is a bit less flexible, and I recommend using the directive (explained below).

In your table component, find the columns with ContentChildren as there are many, and get their cell templates:

@ContentChildren(DatatableColumnComponent) 
columns: QueryList<DatatableColumnComponent>;
cellTemplates: TemplateRef<any>[] = []

// content children available in this hook
ngAfterContentInit() {
  this.cellTemplates = this.columns.toArray().map(c => c.cell.templateRef);

  // like this if not using the cell directive and using TemplateRef directly
  // this.cellTemplates = this.columns.toArray().map(c => c.cell);
}

Then, in your table html, iterate both the data rows AND cell templates, as you need a cell rendered for each column in each row, setting context to $implicit here, but use whatever context is appropriate, (such as {row: row} instead for explicit context):

<tr *ngFor="let row of tableData?.data">
  <ng-container *ngFor="let tpl of cellTemplates">
    <ng-container *ngTemplateOutlet="tpl; context: {$implicit: row}"></ng-container>
  </ng-container>
</tr>

You could also put <td></td> tags inside the column iteration to have actual table cells that enforce column widths.

This is a fairly flexible system... you could envision extending it so you can provide custom column headers in the columns, along with a datatable-header directive to facilitate, and you could find those templates in your column and table components similar to how cells are found, in order to iterate and display those in your header row. Using the directive also allows you to use things other than ng-template to build the template (like a td tag), and can potentially allow you to add more context to your cells via inputs.

Blitz: https://stackblitz.com/edit/angular-gj5ean

Minor edit, realized that ContentChildren are iterable, so you can actually skip the ngAfterContentInit part in the table component, doing:

@ContentChildren(DatatableColumnComponent) 
columns: QueryList<DatatableColumnComponent>;

And just do this in template...

<ng-container *ngFor="let col of columns">
  <ng-container *ngTemplateOutlet="col.cell.templateRef; context: {$implicit: row}"></ng-container>
</ng-container>

Which provides better change detection in the event of the columns themselves changing (eg adding / removing columns)

like image 170
bryan60 Avatar answered Oct 17 '22 05:10

bryan60