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
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.
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.
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.
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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With