简体   繁体   中英

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

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM