Search code examples
angularangular-material

Display tree-like structure using mat-table data with expand/collapse rows


New to Angular and hopefully someone can help me here. I need to display mat-table where there could be multiple rows of data can be expanded/collapsed. Here is the structure I would like display:

export interface Entity {
  name: string;
  age: number;
  children?: Entity[];
}

const Entities: Entity[] = [
  {
    name: 'P1',
    age: 40,
    children: [
      {
        name: 'C1a',
        age: 18
      },
      {
        name: 'C1b',
        protocol: 15      }
    ]
  },
  {
    name: 'P2',
    age: 43,
    children: [
      {
        name: 'C2a',
        age: 21,
      },
      {
        name: 'C2b',
        age: 22
      }
    ]
  }
]

In the above, I want to display rows of like this:

name    age
P1      40
  C1a   18
  C1b   15
P2      43
  C2a   21
  C2b   22

When clicking on P1/P2, rows should expand/collapse. Also, all columns should be aligned. Almost all examples I found use outer table and inner table, but that means columns don't get aligned. Is there a way to use single table for everything or align data in the columns somehow?


Solution

  • We can use MatTreeFlatDataSource for this exact purpose, just add a nested structure as the data source and it will work!

    FULL CODE:

    TS:

    import { FlatTreeControl } from '@angular/cdk/tree';
    import { Component } from '@angular/core';
    import {
      MatTreeFlatDataSource,
      MatTreeFlattener,
    } from '@angular/material/tree';
    
    export interface Entity {
      name: string;
      age: number;
      children?: Entity[];
    }
    
    const TREE_DATA: Entity[] = [
      {
        name: 'P1',
        age: 40,
        children: [
          {
            name: 'C1a',
            age: 18,
          },
          {
            name: 'C1b',
            age: 15,
          },
        ],
      },
      {
        name: 'P2',
        age: 43,
        children: [
          {
            name: 'C2a',
            age: 21,
          },
          {
            name: 'C2b',
            age: 22,
          },
        ],
      },
    ];
    
    interface ExampleFlatNode {
      expandable: boolean;
      name: string;
      age: number;
      level: number;
    }
    
    /**
     * @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[] = ['name', 'age'];
    
      private transformer = (node: Entity, level: number) => {
        return {
          expandable: !!node.children && node.children.length > 0,
          name: node.name,
          age: node.age,
          level: level,
        };
      };
    
      treeControl = new FlatTreeControl<ExampleFlatNode>(
        (node) => node.level,
        (node) => node.expandable
      );
    
      treeFlattener = new MatTreeFlattener(
        this.transformer,
        (node) => node.level,
        (node) => node.expandable,
        (node) => node.children
      );
    
      dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
    
      constructor() {
        this.dataSource.data = TREE_DATA;
      }
    
      hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
    }
    

    HTML:

    <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
      <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef>
          <span [style.paddingLeft.px]="40"> Name </span>
        </th>
        <td mat-cell *matCellDef="let data">
          <button
            mat-icon-button
            [style.visibility]="!data.expandable ? 'hidden' : ''"
            [style.marginLeft.px]="data.level * 32"
            (click)="treeControl.toggle(data)"
          >
            <mat-icon class="mat-icon-rtl-mirror">
              {{treeControl.isExpanded(data) ? 'expand_more' : 'chevron_right'}}
            </mat-icon>
          </button>
    
          {{data.name}}
        </td>
      </ng-container>
    
      <ng-container matColumnDef="age">
        <th mat-header-cell *matHeaderCellDef>Age</th>
        <td mat-cell *matCellDef="let data">{{data.age}}</td>
      </ng-container>
    
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
    

    Stackblitz Demo