Angular Material mat-table Row Grouping
Asked Answered
E

3

31

Leaving aside the libraries that provide row grouping for their particular tables, I am trying to implement such a feature on Angular Material 2 mat-table which does not come with such a feature.

Items to populate the table:

export class BasketItem{
    public id: number;
    public position: number;
    public quantity: number;
    public groupId: number;
} 

Grouping rows that have same groupId property in the following table

 <mat-table class="mat-elevation-z8" [dataSource]="dataSource" multiTemplateDataRows matSort matSortActive="position" matSortDirection="asc" matSortDisableClear >

      <!-- Position Column -->  
      <ng-container matColumnDef="position">
        <mat-header-cell *matHeaderCellDef mat-sort-header>
          <b>Position</b>
        </mat-header-cell>
        <mat-cell *matCellDef="let basketItem">{{basketItem.position}}</mat-cell>
      </ng-container>

      <!-- Quantity Column -->
      <ng-container matColumnDef="quantity">
        <mat-header-cell *matHeaderCellDef>
          <b>Quantity</b>
        </mat-header-cell>
         <mat-cell *matCellDef="let basketItem">{{basketItem.quantity}}</mat-cell>
      </ng-container>

      <!-- GroupId Column -->  
      <ng-container matColumnDef="position">
        <mat-header-cell *matHeaderCellDef mat-sort-header>
          <b>GroupId </b>
        </mat-header-cell>
        <mat-cell *matCellDef="let basketItem">{{basketItem.GroupId }}</mat-cell>
      </ng-container>


      <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>

      <mat-row *matRowDef="let basketItem; columns: displayedColumns;" (click)="onSelect(basketItem)"></mat-row>

    </mat-table>

Any ideas on how the row grouping could be approached?

Entrant answered 7/9, 2018 at 7:12 Comment(1)
Did you managed to use this grouping also with the with the expandable rows ? material.angular.io/components/table/examples see expandable rows from here.Bircher
S
56

A very simple answer would be to sort by the GroupID, this will put those rows together in groups. However, I'm guessing you want a header row displayed before each group.

You can provide an alternative <mat-row *matRowDef="... that uses a where clause. This can be used to display a non-default set of columns. The where clause takes a function that returns true if that matRowDef should be used.

The data you supply to the table would then be the data rows interspersed with group rows, and the function tells one from the other. Taking Basic use of <table mat-table> as a starter, manually add the groups and add the where clause function to app/table-basic-example.ts:

    import {Component} from '@angular/core';

    export interface PeriodicElement {
      name: string;
      position: number;
      weight: number;
      symbol: string;
    }

    export interface Group {
      group: string;
    }

    const ELEMENT_DATA: (PeriodicElement | Group)[] = [
      {group: "Group 1"},
      {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
      {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
      {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
      {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
      {group: "Group 2"},
      {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
      {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
      {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
      {group: "Group 3"},
      {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
      {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
      {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
    ];

    /**
     * @title Basic use of `<table mat-table>`
     */
    @Component({
      selector: 'table-basic-example',
      styleUrls: ['table-basic-example.css'],
      templateUrl: 'table-basic-example.html',
    })
    export class TableBasicExample {
      displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
      dataSource = ELEMENT_DATA;

      isGroup(index, item): boolean{
        return item.group;
      }
    }


    /**  Copyright 2018 Google Inc. All Rights Reserved.
        Use of this source code is governed by an MIT-style license that
        can be found in the LICENSE file at http://angular.io/license */

And add the groupHeader Column and the extra matRowDef to the app/table-basic-example.html:

    <mat-table [dataSource]="dataSource" class="mat-elevation-z8">

      <!--- Note that these columns can be defined in any order.
            The actual rendered columns are set as a property on the row definition" -->

      <!-- Position Column -->
      <ng-container matColumnDef="position">
        <mat-header-cell *matHeaderCellDef> No. </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.position}} </mat-cell>
      </ng-container>

      <!-- Name Column -->
      <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
      </ng-container>

      <!-- Weight Column -->
      <ng-container matColumnDef="weight">
        <mat-header-cell *matHeaderCellDef> Weight </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.weight}} </mat-cell>
      </ng-container>

      <!-- Symbol Column -->
      <ng-container matColumnDef="symbol">
        <mat-header-cell *matHeaderCellDef> Symbol </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.symbol}} </mat-cell>
      </ng-container>

      <ng-container matColumnDef="groupHeader">
        <mat-cell *matCellDef="let group">{{group.group}}</mat-cell>
      </ng-container>

      <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
      <mat-row *matRowDef="let row; columns: ['groupHeader']; when: isGroup"> </mat-row>

    </mat-table>



    <!-- Copyright 2018 Google Inc. All Rights Reserved.
        Use of this source code is governed by an MIT-style license that
        can be found in the LICENSE file at http://angular.io/license -->

Here is a finished stackblitz which groups by the element's initial letter.

And here is a far more developed stackblitz just supply the list of columns you want to group by and it will insert the group rows for you. You can also click the group rows to expand or collapse them

And finally here is a Github project that modifies a copy of the MatTableDataSource class from the material codebase. Works nicely with filter and sort, but 'competes' with the paginator as they both limit the view of records in different ways.

Specht answered 8/10, 2018 at 16:57 Comment(7)
Using your code, how can I only show a certain group, such as the group whose initial starts with 'H'?Talkathon
That's just filtering, look here material.angular.io/components/table/overview#filteringSpecht
I considered filtering, and it works, but I want to display only a certain filtered group by default on when the app opens. Basically I want to subset the datasource object. Any idea how to go about doing that?Talkathon
Hi @guru, that's a really different topic. You should post a new question with some sample code to get your answer.Specht
@StephenTurner I want to filter by Date.However from backend date is of type string.Even if convert it to type date I am getting different row for same date .How do I group same date in one groupBoney
Hi @saad, again that's a really different topic. You should post a new question with some sample code to get your answer.Specht
If I added sorting to the header I had an issue with sorting. the group moves.Eunuchoidism
R
21

Using Stephen Turner's answer, now sharing this stackblitz fork that

  • Dynamicaly discovers columns in the given data
  • Allows to groupby distinct values of the columns on demand

As it was pointed out earlier in this thread,

the easy way to groupBy with Mat-Table is in practice to add rows in the displayed data,

these lines added on start of each new group can be given a custom template with the @Input(matRowDefWhen)

<!-- Default Table lines -->
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

<!-- Group line -->
<tr mat-row *matRowDef="let row; columns: ['groupName']; when: isAGroup"></tr>

In the above example the isAGroup function should return true when a line is a group and not part of the initial data.

Also the groupName column template could be implemented as follows

<ng-container matColumnDef="groupName">
    <td colspan="999" mat-cell *matCellDef="let group">
      {{group.name}}
    </td>
</ng-container>

Finally, if your dataset can vary one could be adding a loop on column template definitions

<ng-container *ngFor="let col of displayedColumns" [matColumnDef]="col">
    <th mat-header-cell *matHeaderCellDef>{{ col }}</th>
    <td mat-cell *matCellDef="let row">{{ row[col] }}</td>
</ng-container>

then hidding and showing the group's lines is just a matter of filtering the displayed data based on the newly hidden group criteria and refreshing the displayed data.

Sorry for this thread necro, only aiming at sharing a piece of reusable code to those looking for a solution.

Red answered 14/8, 2019 at 15:51 Comment(6)
can you contextualize more your answer? show some code data etcJonahjonas
Thanks for your answer, i added markup samples from the previously linked stackblitz.Unlock
Huge work. I 've been able to reuse this, adding cumulable number data on each group rows. Not afraid to say this is master piece work buddy :) thanks a lotSpicy
Thanks for the solution. Does this work with angular CLI 8?Phenformin
Changed <mat-icons></> to <i class=material-icons></i> in my case and it worked.Phenformin
Maybe <mat-icon> didn't work because you didn't import MatIconModule properly. Regarding the compatibility of this solution, it only relies on material.angular.io/components/table/api#MatRowDefUnlock
I
0

You can even break it down further by parsing the compilation and grouping process like so:

  sortStringArray(array: string[], sortOrder: string = 'asc'): string[] {
    if(sortOrder === 'asc'){
      array.sort((a: string, b: string) => a > b ? 1 : a < b ? -1 : 0);
    }
    else if(sortOrder === 'desc'){
      array.sort((a: string, b: string) => a > b ? -1 : a < b ? 1 : 0);
    }
    return array;
  }

  groupStringArray(array: string[], sortOrder: string = 'asc'): string[] {
    let sortedArray = this.sortStringArray(array, sortOrder);
    let groupHeader = '';
    let groupedArray = [];

    for(let i = 0; i < sortedArray.length; i++){
      if(groupHeader !== sortedArray[i]){
        groupHeader = sortedArray[i];
        groupedArray.push({ isGroup: true, value: groupHeader });
        groupedArray.push(sortedArray[i]);
      }
      else{
        groupedArray.push(sortedArray[i]);
      }
    }

    return groupedArray;
  }

...and doing this for each of the different types of values you are addressing and what you expect to return along with fields. This is much more readable and single responsibility to where you can apply these functions elsewhere and easily implement them in your grouping for the table. I have mine set up to where it handles all types and even works recursively with this sort of clean readability. The problem is easier to solve when you 1) realize that sorting first allows for easier finding and setting of the grouping value, 2) that grouping is just realizing where the reference value is no longer constant. This is a brute force method but it is basically what the logic of grouping entails.

Ignatius answered 8/7, 2021 at 3:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.