Search code examples
angularangular-materialexpandable-table

Angular Mat Table Expandable Rows 3-Layer


I am looking at this Stackblitz template https://stackblitz.com/edit/angular-nested-mat-table?file=app%2Ftable-expandable-rows-example.ts

I am trying to create the same table, however, my data has three layers (i.e. User >> Address >> Block)

Does anyone know how/where I should insert the code to enable the Block expandable layer? (see the code I have tried below, it is still not working though)

Thank you

My current code You may copy and paste into the Stackblitz to try

All the code with the word "Sub" or "Block" is written by me

However the "block" layer is still not showing on the table

Does anyone know why?

table-expandable-rows-example.ts

import { Component, ViewChild, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource, MatTable } from '@angular/material/table';

/**
 * @title Table with expandable rows
 */
@Component({
  selector: 'table-expandable-rows-example',
  styleUrls: ['table-expandable-rows-example.css'],
  templateUrl: 'table-expandable-rows-example.html',
  animations: [
    trigger('detailExpand', [
      state('collapsed', style({ height: '0px', minHeight: '0' })),
      state('expanded', style({ height: '*' })),
      transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
    ]),
  ],
})
export class TableExpandableRowsExample {

  @ViewChild('outerSort', { static: true }) sort: MatSort;
  @ViewChildren('innerSort') innerSort: QueryList<MatSort>;
  @ViewChildren('innerTables') innerTables: QueryList<MatTable<Address>>;

  dataSource: MatTableDataSource<User>;
  usersData: User[] = [];
  columnsToDisplay = ['name', 'email', 'phone'];
  innerDisplayedColumns = ['street', 'zipCode', 'city'];
  subBlockDisplayedColumns = ['name', 'level', 'unitnumber'];
  expandedElement: User | null;
  expandedSubElement: Address | null;

  constructor(
    private cd: ChangeDetectorRef
  ) { }

  ngOnInit() {
    USERS.forEach(user => {
      if (user.addresses && Array.isArray(user.addresses) && user.addresses.length) {
        this.usersData = [...this.usersData, {...user, addresses: new MatTableDataSource(user.addresses)}];
      } else {
        this.usersData = [...this.usersData, user];
      }
    });
    this.dataSource = new MatTableDataSource(this.usersData);
    this.dataSource.sort = this.sort;
  }

  toggleRow(element: User) {
    element.addresses && (element.addresses as MatTableDataSource<Address>).data.length ? (this.expandedElement = this.expandedElement === element ? null : element) : null;
    this.cd.detectChanges();
    this.innerTables.forEach((table, index) => (table.dataSource as MatTableDataSource<Address>).sort = this.innerSort.toArray()[index]);
  }

  toggleSubRow(element: Address) {
    element.blocks && (element.blocks as MatTableDataSource<Block>).data.length ? (this.expandedSubElement = this.expandedSubElement === element ? null : element) : null;
    this.cd.detectChanges();
    this.innerTables.forEach((table, index) => (table.dataSource as MatTableDataSource<Address>).sort = this.innerSort.toArray()[index]);
  }

  applyFilter(filterValue: string) {
    this.innerTables.forEach((table, index) => (table.dataSource as MatTableDataSource<Address>).filter = filterValue.trim().toLowerCase());
  }
}

export interface User {
  name: string;
  email: string;
  phone: string;
  addresses?: Address[] | MatTableDataSource<Address>;
}

export interface Address {
  street: string;
  zipCode: string;
  city: string;
  blocks?: Block[] | MatTableDataSource<Block>;
}

export interface Block {
  name: string;
  level: string;
  unitnumber: string;
}

export interface UserDataSource {
  name: string;
  email: string;
  phone: string;
  addresses?: MatTableDataSource<Address>;
}

const USERS: User[] = [
  {
    name: "Mason",
    email: "[email protected]",
    phone: "9864785214",
    addresses: [
      {
        street: "Street 1",
        zipCode: "78542",
        city: "Kansas",
        blocks: [
          {
            name: "Blk 11",
            level: "Lvl 1",
            unitnumber: "01-01"
          },
          {
            name: "Blk 22",
            level: "Lvl 2",
            unitnumber: "02-01"
          }
        ]
      },
      {
        street: "Street 2",
        zipCode: "78554",
        city: "Texas",
        blocks: [
          {
            name: "Blk 33",
            level: "Lvl 3",
            unitnumber: "03-02"
          },
          {
            name: "Blk 44",
            level: "Lvl 4",
            unitnumber: "04-02"
          }
        ]
      }
    ]
  },
  {
    name: "Jason",
    email: "[email protected]",
    phone: "7856452187",
    addresses: [
      {
        street: "Street 5",
        zipCode: "23547",
        city: "Utah"
      },
      {
        street: "Street 5",
        zipCode: "23547",
        city: "Ohio"
      }
    ]
  }
];


/**  Copyright 2019 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 */

table-expandable-rows-example.html

<table mat-table #outerSort="matSort" [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8" matSort>
    <ng-container matColumnDef="{{column}}" *ngFor="let column of columnsToDisplay">
        <th mat-header-cell *matHeaderCellDef mat-sort-header> {{column}} </th>
        <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
    </ng-container>

    <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
    <ng-container matColumnDef="expandedDetail">
        <td mat-cell *matCellDef="let element" [attr.colspan]="columnsToDisplay.length">
            <div class="example-element-detail" *ngIf="element.addresses?.data.length" [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
                <div class="inner-table mat-elevation-z8" *ngIf="expandedElement">
          <mat-form-field>
            <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
          </mat-form-field>
          <table #innerTables mat-table #innerSort="matSort" [dataSource]="element.addresses" matSort>
            <ng-container matColumnDef="{{innerColumn}}" *ngFor="let innerColumn of innerDisplayedColumns">
              <th mat-header-cell *matHeaderCellDef mat-sort-header> {{innerColumn}} </th>
              <td mat-cell *matCellDef="let element"> {{element[innerColumn]}} </td>
            </ng-container>
            <tr mat-header-row *matHeaderRowDef="innerDisplayedColumns"></tr>
            <tr mat-row *matRowDef="let row; columns: innerDisplayedColumns;"></tr>
          </table>
                </div>
            </div>
        </td>
    </ng-container>

    <!-- Expanded SubContent Column - The subdetail row is made up of this one column that spans across all columns -->
    <ng-container matColumnDef="expandedSubDetail">
    <td mat-cell *matCellDef="let element" [attr.colspan]="innerDisplayedColumns.length">
        <div class="example-element-detail" *ngIf="element.blocks?.data.length" [@detailExpand]="element == expandedSubElement ? 'expanded' : 'collapsed'">
            <div class="inner-table mat-elevation-z8" *ngIf="expandedSubElement">
                <table #innerTables mat-table #innerSort="matSort" [dataSource]="element.addresses" matSort>
                    <ng-container matColumnDef="{{innerColumn}}" *ngFor="let innerColumn of innerDisplayedColumns">
                        <th mat-header-cell *matHeaderCellDef mat-sort-header> {{innerColumn}} </th>
                        <td mat-cell *matCellDef="let element"> {{element[innerColumn]}} </td>
                    </ng-container>
                    <tr mat-header-row *matHeaderRowDef="subBlockDisplayedColumns"></tr>
                    <tr mat-row *matRowDef="let row; columns: subBlockDisplayedColumns;"></tr>
                </table>
            </div>
        </div>
    </td>
</ng-container>
    
    <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
    <tr mat-row *matRowDef="let element; columns: columnsToDisplay;" [class.example-element-row]="element.addresses?.data.length"
     [class.example-expanded-row]="expandedElement === element" (click)="toggleRow(element)">
    </tr>
    <tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"  [class.example-expanded-row]="expandedSubElement === element" (click)="toggleSubRow(element)"></tr>
    <tr mat-row *matRowDef="let row; columns: ['expandedSubDetail']" class="example-detail-row"></tr>

</table>


<!-- Copyright 2019 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 -->

Solution

  • I got it to work, these are the major things I did

    • Move the blocks table inside the address table
    • While assigning the dataSource I have added blocks as a data source
    • Added separate template references for address and block elements
    • Call toggleSubRow() when the inner table mat-row is clicked.

    Copy the below code to your stackblitz example. Hope this helps.

    <table
      mat-table
      #outerSort="matSort"
      [dataSource]="dataSource"
      multiTemplateDataRows
      class="mat-elevation-z8"
      matSort
    >
      <ng-container
        matColumnDef="{{column}}"
        *ngFor="let column of columnsToDisplay"
      >
        <th mat-header-cell *matHeaderCellDef mat-sort-header>{{column}}</th>
        <td mat-cell *matCellDef="let element">{{element[column]}}</td>
      </ng-container>
    
      <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
      <ng-container matColumnDef="expandedDetail">
        <td
          mat-cell
          *matCellDef="let element"
          [attr.colspan]="columnsToDisplay.length"
        >
          <div
            class="example-element-detail"
            *ngIf="element.addresses?.data.length"
            [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'"
          >
            <div class="inner-table mat-elevation-z8" *ngIf="expandedElement">
              <mat-form-field>
                <input
                  matInput
                  (keyup)="applyFilter($event.target.value)"
                  placeholder="Filter"
                />
              </mat-form-field>
              <table
                #innerTables
                mat-table
                #innerSort="matSort"
                [dataSource]="element.addresses"
                matSort
                multiTemplateDataRows
              >
                <ng-container
                  matColumnDef="{{innerColumn}}"
                  *ngFor="let innerColumn of innerDisplayedColumns"
                >
                  <th mat-header-cell *matHeaderCellDef mat-sort-header>
                    {{innerColumn}}
                  </th>
                  <td mat-cell *matCellDef="let element">
                    {{element[innerColumn]}}
                  </td>
                </ng-container>
    
                <!-- Expanded SubContent Column - The subdetail row is made up of this one column that spans across all columns -->
                <ng-container matColumnDef="expandedSubDetail">
                  <td
                    mat-cell
                    *matCellDef="let element"
                    [attr.colspan]="innerDisplayedColumns.length"
                  >
    
                    <div
                      class="example-element-detail"
                      *ngIf="element?.blocks?.data.length"
                      [@detailExpand]="element == expandedSubElement ? 'expanded' : 'collapsed'"
                    >
    
                      <div
                        class="inner-table mat-elevation-z8"
                        *ngIf="expandedSubElement"
                      >
                        <table
                          #subTables
                          mat-table
                          #subSort="matSort"
                          [dataSource]="element.blocks"
                          matSort
                        >
                          <ng-container
                            matColumnDef="{{innerColumn}}"
                            *ngFor="let innerColumn of subBlockDisplayedColumns"
                          >
                            <th mat-header-cell *matHeaderCellDef mat-sort-header>
                              {{innerColumn}}
                            </th>
                            <td mat-cell *matCellDef="let element">
                              {{element[innerColumn]}}
                            </td>
                          </ng-container>
                          <tr
                            mat-header-row
                            *matHeaderRowDef="subBlockDisplayedColumns"
                          ></tr>
                          <tr
                            mat-row
                            *matRowDef="let row; columns: subBlockDisplayedColumns;"
                          ></tr>
                        </table>
                      </div>
                    </div>
                  </td>
                </ng-container>
    
                <tr mat-header-row *matHeaderRowDef="innerDisplayedColumns"></tr>
                <tr
                  mat-row
                  *matRowDef="let row; columns: innerDisplayedColumns;"
                  (click)="toggleSubRow(row)"
                ></tr>
                <tr
                  mat-row
                  *matRowDef="let row; columns: ['expandedSubDetail']"
                  class="example-detail-row"
                ></tr>
              </table>
            </div>
          </div>
        </td>
      </ng-container>
    
      <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
      <tr
        mat-row
        *matRowDef="let element; columns: columnsToDisplay;"
        [class.example-element-row]="element.addresses?.data.length"
        [class.example-expanded-row]="expandedElement === element"
        (click)="toggleRow(element)"
      ></tr>
      <tr
        mat-row
        *matRowDef="let row; columns: ['expandedDetail']"
        class="example-detail-row"
        [class.example-expanded-row]="expandedSubElement === element"
      ></tr>
    </table>
    
    import {
      Component,
      ViewChild,
      ViewChildren,
      QueryList,
      ChangeDetectorRef,
    } from '@angular/core';
    import {
      animate,
      state,
      style,
      transition,
      trigger,
    } from '@angular/animations';
    import { MatSort } from '@angular/material/sort';
    import { MatTableDataSource, MatTable } from '@angular/material/table';
    
    /**
     * @title Table with expandable rows
     */
    @Component({
      selector: 'table-expandable-rows-example',
      styleUrls: ['table-expandable-rows-example.css'],
      templateUrl: 'table-expandable-rows-example.html',
      animations: [
        trigger('detailExpand', [
          state('collapsed', style({ height: '0px', minHeight: '0' })),
          state('expanded', style({ height: '*' })),
          transition(
            'expanded <=> collapsed',
            animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')
          ),
        ]),
      ],
    })
    export class TableExpandableRowsExample {
      @ViewChild('outerSort', { static: true }) sort: MatSort;
      @ViewChildren('innerSort') innerSort: QueryList<MatSort>;
      @ViewChildren('subSort') subSort: QueryList<MatSort>;
      @ViewChildren('innerTables') innerTables: QueryList<MatTable<Address>>;
      @ViewChildren('subTables') subTables: QueryList<MatTable<Block>>;
    
      dataSource: MatTableDataSource<User>;
      usersData: User[] = [];
      columnsToDisplay = ['name', 'email', 'phone'];
      innerDisplayedColumns = ['street', 'zipCode', 'city'];
      subBlockDisplayedColumns = ['name', 'level', 'unitnumber'];
      expandedElement: User | null;
      expandedSubElement: Address | null;
    
      constructor(private cd: ChangeDetectorRef) {}
    
      ngOnInit() {
        USERS.forEach((user) => {
          if (
            user.addresses &&
            Array.isArray(user.addresses) &&
            user.addresses.length
          ) {
            const addresses: Address[] = [];
    
            user.addresses.forEach((address) => {
              if (Array.isArray(address.blocks)) {
                addresses.push({
                  ...address,
                  blocks: new MatTableDataSource(address.blocks),
                });
              }
            });
    
            this.usersData.push({
              ...user,
              addresses: new MatTableDataSource(addresses),
            });
          } else {
            this.usersData = [...this.usersData, user];
          }
        });
        this.dataSource = new MatTableDataSource(this.usersData);
        this.dataSource.sort = this.sort;
      }
    
      toggleRow(element: User) {
        element.addresses &&
        (element.addresses as MatTableDataSource<Address>).data.length
          ? (this.expandedElement =
              this.expandedElement === element ? null : element)
          : null;
    
        this.cd.detectChanges();
        this.innerTables.forEach(
          (table, index) =>
            ((table.dataSource as MatTableDataSource<Address>).sort =
              this.innerSort.toArray()[index])
        );
      }
    
      toggleSubRow(element: Address) {
        element.blocks && (element.blocks as MatTableDataSource<Block>).data.length
          ? (this.expandedSubElement =
              this.expandedSubElement === element ? null : element)
          : null;
    
        this.cd.detectChanges();
        this.subTables.forEach(
          (table, index) =>
            ((table.dataSource as MatTableDataSource<Block>).sort =
              this.subSort.toArray()[index])
        );
      }
    
      applyFilter(filterValue: string) {
        this.innerTables.forEach(
          (table, index) =>
            ((table.dataSource as MatTableDataSource<Address>).filter = filterValue
              .trim()
              .toLowerCase())
        );
      }
    }
    
    export interface User {
      name: string;
      email: string;
      phone: string;
      addresses?: Address[] | MatTableDataSource<Address>;
    }
    
    export interface Address {
      street: string;
      zipCode: string;
      city: string;
      blocks?: Block[] | MatTableDataSource<Block>;
    }
    
    export interface Block {
      name: string;
      level: string;
      unitnumber: string;
    }
    
    export interface UserDataSource {
      name: string;
      email: string;
      phone: string;
      addresses?: MatTableDataSource<Address>;
    }
    
    const USERS: User[] = [
      {
        name: 'Mason',
        email: '[email protected]',
        phone: '9864785214',
        addresses: [
          {
            street: 'Street 1',
            zipCode: '78542',
            city: 'Kansas',
            blocks: [
              {
                name: 'Blk 11',
                level: 'Lvl 1',
                unitnumber: '01-01',
              },
              {
                name: 'Blk 22',
                level: 'Lvl 2',
                unitnumber: '02-01',
              },
            ],
          },
          {
            street: 'Street 2',
            zipCode: '78554',
            city: 'Texas',
            blocks: [
              {
                name: 'Blk 33',
                level: 'Lvl 3',
                unitnumber: '03-02',
              },
              {
                name: 'Blk 44',
                level: 'Lvl 4',
                unitnumber: '04-02',
              },
            ],
          },
        ],
      },
      {
        name: 'Jason',
        email: '[email protected]',
        phone: '7856452187',
        addresses: [
          {
            street: 'Street 5',
            zipCode: '23547',
            city: 'Utah',
          },
          {
            street: 'Street 5',
            zipCode: '23547',
            city: 'Ohio',
          },
        ],
      },
    ];