Reputation: 27516
I'm trying to create a set of components that will allow me to render a table, where the "user" only needs to provide:
So, ideally, you might use these components to render a 3-column table like so:
<app-table [data]='sampleData'>
<td>{{cell.toString().toUpperCase()}}</td>
<td class="myClass">{{cell}}</td>
<app-cell [cell]="cell"></app-cell>
</app-table>
...the idea being that the app-table
component would render the three app-column
components for every row, and magically provide the cell value to each column as cell
. The user of the component then has full control over how each column is rendered - including what classes are applied to the td
tag, etc.
I've not had much luck trying to achieve this. The best I've come up with so far can be seen in this StackBlitz, where I have an app-table
component that renders the tr
, and which expects its content to include one ng-template
for each column. The markup is not nearly as clean as the pseudo-markup above:
<app-table [data]='sampleData'>
<ng-template let-cell="cell"><td>{{cell.toString().toUpperCase()}}</td></ng-template>
<ng-template let-cell="cell"><td class="myClass">{{cell}}</td></ng-template>
<ng-template let-cell="cell"><td><app-cell [data]="cell"></app-cell></td></ng-template>
</app-table>
It's a bit ugly because of all the extra markup required to pass the data down into the template, but that's not the biggest problem.
You'll notice that the last "column" uses an app-cell
component, but that component is still wrapped inside a td
tag:
<ng-template let-cell="cell">
<td>
<app-cell [data]="cell"></app-cell>
</td>
</ng-template>
That's not really what I wanted: I'd like the app-cell
component to provide the td
tag itself, since otherwise it can't (for example) set classes on that element.
Currently, the app-cell
component has a HostBinding
that adds a class ("foo"), but that gets added to the app-cell
element itself, not to the td
element where it's needed:
<td>
<app-cell class="foo">
<div>...</div>
</app-cell>
</td>
The obvious thing to do is to move the td
tag into the app-cell
component, but if I do that, then the rendered HTML looks like this:
<app-cell class="foo">
<td>
<div>...</div>
</td>
</app-cell>
[OK, so the class is still applied in the wrong place, but it's easy to see how to move it down to the td
tag.]
However, that extra pseudo-element sitting between the tr
and td
elements worries me - the browser I'm using doesn't seem to mind, but I suspect that much of my CSS is going to be thrown by it, because there are selectors that expect the td
to be a direct child of the tr
.
Is there a way to get rid of that extra pseudo-element? Ordinarily I might look at using a directive, but I don't see how I can do that here, because the app-cell
component needs a template, and directives don't have those...
Upvotes: 3
Views: 498
Reputation: 8859
These types of components are my favorite. At the end of the day, you want something like following assuming you have a data like this
carData = [{
model: 'Mercedes',
year: 2015,
km: 10000
}, {
model: 'Audi',
year: 2016,
km: 5000
}];
And you want to display first two columns as is and for the last one you want to display some custom template.
<app-table [data]="carData">
<app-cell header="Model" field="model"></app-cell>
<app-cell header="Year" field="year"></app-cell>
<app-cell header="Km" field="km" styleClass="km-cell">
<ng-template custom-cell-body let-car>
{{car.km | number}} km
</ng-template>
</app-cell>
</app-table>
Here is what you can do, (I got this trick out of Angular Material and Primeng which works just fine)
You can find a working example here
First define a directive for custom body templates for cells. (You can do the same thing for headers as well)
@Directive({selector: '[custom-cell-body]'})
export class AppCellBodyTemplate {
constructor(public template: TemplateRef<any>) {}
}
And let's define our AppCellComponent
app-cell.component.ts
@Component({
selector: 'app-cell',
template: `<ng-content></ng-content>`
})
export class AppCellComponent {
@Input() header;
@Input() field;
@Input() styleClass;
@ContentChild(AppCellBodyTemplate) customBody: AppCellBodyTemplate;
}
Let's glue them all together at AppTableComponent
app-table.component.ts
@Component({
selector: 'app-table',
templateUrl: './app-table.component.html'
})
export class AppTableComponent {
@Input() data: any[];
@ContentChildren(AppCellComponent) cells: QueryList<AppCellComponent>;
}
app-table.component.html
<table>
<thead>
<tr>
<!-- loop through cells and create header part of the table -->
<ng-container *ngFor="let cell of cells">
<th>{{cell.header}}</th>
</ng-container>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data">
<!-- loop through cells -->
<ng-container *ngFor="let cell of cells">
<td [ngClass]="cell.styleClass">
<!-- if cell has a custom body, render that -->
<ng-container *ngIf="cell.customBody; else defaultBody">
<ng-container *ngTemplateOutlet="cell.customBody.template;
context: {$implicit: row}">
</ng-container>
</ng-container>
<!-- else render default cell body -->
<ng-template #defaultBody>{{row[cell.field]}}</ng-template>
</td>
</ng-container>
</tr>
</tbody>
</table>
Here are some points needed to be explained.
template: TemplateRef<any>
within AppCellBodyTemplate
Since, we are going to use custom-body-cell
with ng-template
s all the time, we can get a reference of the template defined within. This template
will be used within *ngTemplateOutlet
@ContentChild(AppCellBodyTemplate) customBody
within AppCellComponent
is to detect whether there is a custom-body-cell
defined as ContentChild
@ContentChildren(AppCellComponent) cells: QueryList<AppCellComponent>
is to get instance of cells
(you did the similar thing).context: {$implicit: row}
is to expose some data to the consumers of this component. $implicit
makes you enable to retrieve this context with any template variable you want. Since, I used $implicit
here, I was able to do this <ng-template custom-cell-body let-car>
Otherwise, I would have to define my car
variable something like this let-car="rowData"
Upvotes: 3
Reputation: 27516
Answering my own question - because I found an answer in the "related" sidebar: I can use an attribute selector rather than an element selector.
This StackBlitz shows the results.
Upvotes: 0