6

I know a similar question to this is all over the site, but my problem is slightly different from those.

I want to write a component that serves rows, but each instance may generate from one to many rows, depending on the provided data:

<tbody>
    <!-- intended usage -->
    <data-rows *ngFor="let row of dataRows" [row]="row">
    </data-rows>
</tbody>

Needs to create:

<tbody>

    <!-- from first <data-rows> instance -->
    <tr><td> instance 1, row 1 </td></tr>

    <!-- from second <data-rows> instance -->
    <tr><td> instance 2, row 1 </td></tr>
    <tr><td> instance 2, row 2 </td></tr>
    <tr><td> instance 2, row 3 </td></tr>

    <!-- from third <data-rows> instance -->
    <tr><td> instance 3, row 1 </td></tr>

</tbody>

Most solutions propose using an attribute selector and use *ngFor on a real <tr>. This will not work in my case because there's not a one-to-one relationship between instances and rows. In addition, the parent component doesn't know how many <tr> should be rendered; that's for <data-rows> to decide.

Of course a naive implementation of <data-rows> would fail, as it would add unsupported elements to the tbody:

<tbody>

    <data-rows>
        <!-- from first <data-rows> instance -->
        <tr><td> instance 1, row 1 </td></tr>
    </data-rows>

    <data-rows>
        <!-- from second <data-rows> instance -->
        <tr><td> instance 2, row 1 </td></tr>
        <tr><td> instance 2, row 2 </td></tr>
        <tr><td> instance 2, row 3 </td></tr>
    </data-rows>

    <data-rows>
        <!-- from third <data-rows> instance -->
        <tr><td> instance 3, row 1 </td></tr>
    </data-rows>

</tbody>

That doesn't work because tbody can only contain <tr> elements, so the table logic breaks.

My intuition (as that of many who asked a similar question) is to render <data-rows> without the actual <data-rows> element (only its contents) but I think it might not be supported because the css emulate mode would break.

What's a good way of solving this without breaking the initial premise?

Guillermo Prandi
  • 1,537
  • 1
  • 16
  • 31

2 Answers2

3

As pointed out by user charlietfl in the comments, I can use multiple TBODY tags (never occurred to me), so in my case I can easily generate:

<tbody data-rows class="instance-1">
    <tr><td> instance 1, row 1 </td></tr>
</tbody>

<tbody data-rows class="instance-2">
    <tr><td> instance 2, row 1 </td></tr>
    <tr><td> instance 2, row 2 </td></tr>
    <tr><td> instance 2, row 3 </td></tr>
</tbody>

<tbody data-rows class="instance-3">
    <tr><td> instance 3, row 1 </td></tr>
</tbody>

In my component, the selector is tbody[data-rows], and I can have a parent template like this:

<table>
    <thead>
        <tr><th>Table header</th></tr>
    </thead>
    <tbody data-rows *ngFor="let row of data" [row]="row"></tbody>
</table>

This method has the additional advantage of allowing an even/odd styling on an instance (tbody) basis, which in my case makes sense.

The row generator's template is simply another ngIf/ngFor driven template as needed:

<tr><td>This row will always be shown</td></tr>
<tr *ngIf="..."><td>This row, only if first expansion is needed</td></tr>
<tr *ngIf="..."><td>This row, only if second expansion is needed</td></tr>
<tr *ngFor="..."><td>More additional rows</td></tr>

etc.

Guillermo Prandi
  • 1,537
  • 1
  • 16
  • 31
0

You could use a 2 dimensional array: Array<Array<number>> where the 1st dimension is the quantity of data-rows, the second is the number of <tr> to render.

For example:

<data-row *ngFor="let row of array2d" [rowQuantity]="row" ></data-row>

and inside your data-row component:

<tr *ngFor="let tr of rowQuantity"></tr>
Zze
  • 18,229
  • 13
  • 85
  • 118
  • 1
    you can use ng-container (Angular doesn't put it in the DOM) see https://angular.io/guide/structural-directives#ng-container-to-the-rescue – Eliseo Jan 16 '18 at 22:44
  • Thank you, but the number of rows the component renders is dynamic, i.e. changes after it's instantiated after user interaction. The parent has no way of knowing in advance how many rows there will be, and should be agnostic about it. – Guillermo Prandi Jan 16 '18 at 22:54
  • @Eliseo I'll try ng-container. It's currently working with multiple TBODY elements as suggested by charlietfl above. Ng-container sounds lighter. – Guillermo Prandi Jan 16 '18 at 22:57
  • @GuillermoPrandi *"The parent has no way of knowing in advance how many rows there will be"* - Angular is databound so why does this matter (the table doesn't know how many ``'s it's about to have either)? as soon as the user instantiates a new one, update the array which will trigger `ngOnChanges` and then the view will update to display the new rows. – Zze Jan 16 '18 at 23:05
  • @Zze The parent must not know how many rows there are per children for modularity purposes. I cannot move the expansion logic to the parent because that would mean that the parent messes with the internal structure of the child. Besides, I'd rather not redraw the whole table each time (I plan to use an animation for the appearing/dissapearing rows). – Guillermo Prandi Jan 17 '18 at 00:20
  • @Eliseo I couldn't make it work with ng-container; it doesn't accept the attribute selector (e.g. can't bind to 'inputValue' since it isn't a known property of 'ng-container'.). I'm unable to bind parameters to the child without instancing an element on the parent either way. But it's working now with multiple TBODY, so thank you! – Guillermo Prandi Jan 17 '18 at 14:23