angularhtml-tableprimeng

PrimeNG table with grouped columns won't sort


PrimeNG tables can be sortable by column. https://www.primefaces.org/primeng/#/table/sort

They can also have column groups. https://www.primefaces.org/primeng/#/table/colgroup

Unfortunately, I'm having trouble getting these two features to work together. The example code for the former simplifies the creation process with dynamic column generation, but the example code for the latter requires that each column be coded manually.

I have a way of writing the code that seems to me like it should work, and it builds and displays without errors; BUT it will not sort the data when the sorting icon is clicked.

Here's my code (stripped of identifying variable names):

<div *ngIf="(sourceObservableReturningAnArray$ | async) as arrayOfDataNeeded">
  <p-table
    [value]="arrayOfDataNeeded"
    autoLayout="true"
    sortField="firstSortedProperty"
    [rows]="25">
    <ng-template pTemplate="header" let-columns>
      <tr>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.firstSortedProperty">
          Header for First Property
          <p-sortIcon [field]="arrayOfDataNeeded.firstSortedProperty"></p-sortIcon>
        </th>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.secondSortedProperty">
          Header for Second Property
          <p-sortIcon [field]="arrayOfDataNeeded.secondSortedProperty"></p-sortIcon>
        </th>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.thirdSortedProperty">
            Header for Third Property
            <p-sortIcon [field]="arrayOfDataNeeded.thirdSortedProperty"></p-sortIcon>
        </th>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.fourthSortedProperty">
            Header for Fourth Property
            <p-sortIcon [field]="arrayOfDataNeeded.fourthSortedProperty"></p-sortIcon>
        </th>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.fifthSortedProperty">
            Header for Fifth Property
            <p-sortIcon [field]="arrayOfDataNeeded.fifthSortedProperty"></p-sortIcon>
        </th>
        <th [pSortableColumn]="arrayOfDataNeeded.sixthSortedProperty" colspan="2">
            Header with Subheadings
            <p-sortIcon [field]="arrayOfDataNeeded.sixthSortedProperty"></p-sortIcon>
        </th>
        <th rowspan="2" [pSortableColumn]="arrayOfDataNeeded.seventhSortedProperty">
            Header for Seventh Property
            <p-sortIcon [field]="arrayOfDataNeeded.seventhSortedProperty"></p-sortIcon>
        </th>
      </tr>
      <tr>
        <th>Subheading 1</th>
        <th>Subheading 2</th>
      </tr>
    </ng-template>
    <ng-template pTemplate="body" let-data>
        <tr>
            <td>{{data.firstSortedProperty | currency}}</td>
            <td>{{data.secondSortedProperty}}</td>
            <td>{{data.thirdSortedProperty | date : 'shortTime'}}<br>{{data.thirdProperty | date : 'MM/dd/yyyy'}}</td>
            <td>{{data.fourthSortedProperty}}%</td>
            <td>{{data.fifthSortedProperty}}%</td>
            <td>{{data.sixthSortedProperty}}%</td>
            <td>{{data.additionalUnsortedProperty | currency}}</td>
            <td>{{data.seventhSortedProperty}}</td>
        </tr>
    </ng-template>
  </p-table>
</div>

I'm guessing that the problem is somewhere in the specification of the sorting field: since arrayOfDataNeeded is an array of objects, but it's the objects that have the properties I want to sort on, perhaps I'm not identifying the object's relevant property in the correct manner. I'm not familiar enough with PrimeNG to know how I should do it otherwise. It's possible that I can fix it by doing something with the implicit context, but I'm not sure what.

The sortField="firstSortedProperty" in the opening tag works exactly as expected, so it doesn't seem to be a problem with the data source.

If you see my error, please point it out! I'm happy to answer clarifying questions if there's anything I've left out of the problem's description. I'm also open to suggestions about alternative ways of implementing the needed functionality.


Solution

  • So there are [at least] two solutions to my original question. The first one is quick, but has a code smell. The second one is more involved, but also likely to be the correct way to go about solving the problem I posed.


    FIRST SOLUTION

    1. First, add the following to the component.ts file:
    cols: any[];
    

    and

    this.cols = [
          { field: 'firstSortedProperty', header: 'Header for First Property'},
          { field: 'secondSortedProperty', header: 'Header for Second Property'},
          { field: 'thirdSortedProperty', header: 'Header for Third Property'},
          { field: 'fourthSortedProperty', header: 'Header for Fourth Property'},
          { field: 'fifthSortedProperty', header: 'Header for Fifth Property'},
          { field: 'sixthSortedProperty', header: 'Header for Sixth Property'},
          { field: 'additionalUnsortedProperty', header: '[It Does Not Show, So Whatever]'},
          { field: 'seventhSortedProperty', header: 'Header for Seventh Property'},
    ];
    
    1. Then, in the component.html file, add [columns]="cols" to the p-table properties.
    <p-table
        [value]="arrayOfDataNeeded"
        [columns]="cols"
        autoLayout="true"
        sortField="firstSortedProperty"
        [rows]="25">
    

    Explanation: By providing the p-table with a value for the columns property, the field names found in each column become accessible. The columns property is supposed to contain an array from which the columns will be dynamically generated, but you're not doing that. (Tricksy!) It works at the moment, though there's no telling if future changes to PrimeNG will break this workaround.


    SECOND SOLUTION

    It's actually possible to dynamically generate columns while simultaneously using headings and subheadings.

    1. In order to make it happen, we have to add more properties to the objects in our cols array.
    this.cols = [
          { field: 'firstSortedProperty', header: 'Header for First Property', hasSubs: false, isSub: false},
          { field: 'secondSortedProperty', header: 'Header for Second Property', hasSubs: false, isSub: false},
          { field: 'thirdSortedProperty', header: 'Header for Third Property', hasSubs: false, isSub: false},
          { field: 'fourthSortedProperty', header: 'Header for Fourth Property', hasSubs: false, isSub: false},
          { field: 'fifthSortedProperty', header: 'Header for Fifth Property', hasSubs: false, isSub: false},
          { field: 'sixthSortedProperty', header: 'Header for Sixth Property', hasSubs: true, isSub: false},
          { field: 'additionalUnsortedProperty', header: '[It Doesn't Show, So Whatever]', hasSubs: false, isSub: true},
          { field: 'seventhSortedProperty', header: 'Header for Seventh Property', hasSubs: false, isSub: false},
    ];
    

    Notice that the hasSubs property is true for the sixthSortedProperty. This property indicates that the heading has subheadings, and will be used in the html file to alter the creation of the .

    The additionalUnsortedProperty object could be omitted from the array, but I'm including it here to point you in the direction of how you could go ahead with dynamically generating the subheadings. The solution I'm providing still specifies the subheadings statically, since I'm trying to focus on the essential elements of the solution. That extra bit is left as an exercise for the reader. (I'm lazy?)

    1. Next, we need to change the way columns are generated in the component.html file. We'll use *ngIf to provide one sort of formatting to columns without subheadings, and another for columns with subheadings.
    <ng-template pTemplate="header" let-columns>
      <tr [hidden]="loadOffersTable.isEmpty()">
        <th></th>
        <ng-container *ngFor="let col of columns">
          <ng-container *ngIf="col.hasSubs; else noSubs">
            <th colspan="2" [pSortableColumn]="col.field">
              {{col.header}}
              <p-sortIcon [field]="col.field" ariaLabel="Activate to sort" ariaLabelDesc="Activate to sort in descending order" ariaLabelAsc="Activate to sort in ascending order"></p-sortIcon>
            </th>
          </ng-container>
          <ng-template #noSubs>
            <th *ngIf="!col.isSub" rowspan="2" [pSortableColumn]="col.field">
              {{col.header}}
              <p-sortIcon [field]="col.field"></p-sortIcon>
            </th>
          </ng-template>
        </ng-container>
      </tr>
      <tr>
        <th></th>
        <th>Subheading 1</th>
        <th>Subheading 2</th>
      </tr>
    </ng-template>
    

    The difference in formatting between the two columns is primarily a matter of the colspan and rowspan properties we set on either. If it has subheadings, we set colspan="2" so that the heading spans both of the subcolumns. If it has no subheadings, we set rowspan="2" so that the heading fills the subheading row too (for that column). (Unfortunately, you can't try anything super-fancy by using both rowspan and colspan in the same . It has to be one or the other.)

    Also note the use of logical containers in order to handle the *ngFor outside of the conditional setup. You need the *ngFor in order to iterate through the columns, but you can't set up the table headers conditionally if the *ngFor is inside the tag (like it is in the examples that are usually given for dynamic column generation).

    1. The subheading code will actually remain the same. If you want to spruce up the code so that the subheadings are also generated dynamically, it can be done mutatis mutandis.

    Now that everything's generated dynamically, it opens up options for such things as:


    BONUS!

    You might still think you need static columns because your table design requires data in different columns to be formatted or piped differently. (It's a good chance that you're a bare newbie if you think that, but we all start out that way.) Fortunately, you've already set things up to fix that problem with the cols array. Use [ngSwitch] with the field property in order to identify which column's data is being displayed in the cell, and format the cell accordingly. You still need to iterate through the columns, but now it's okay for the *ngFor to appear in the tag. The logical container will now appear inside the cell, and determine how the cell's contents are displayed. Here's the beginning of what you'd do:

    <ng-template pTemplate="body" let-nameForDataObject let-columns="columns">
      <tr>
        <td *ngFor="let col of columns">
          <ng-container [ngSwitch]="col.field">
            <ng-container *ngSwitchCase="'firstSortedProperty'">{{nameForDataObject.firstSortedProperty}}</ng-container>
            <ng-container *ngSwitchCase="'secondSortedProperty'">{{nameForDataObject.secondSortedProperty}}</ng-container>
    

    ...and so on. (Using a switch statement in order to format table data isn't a clever trick or a new trick, but it might not be obvious to someone new to PrimeNG how to make it work.)


    I hope these solutions help you get your PrimeNG table set up quickly and correctly. Good luck!