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: "mason@test.com",
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: "jason@test.com",
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 -->
I got it to work, these are the major things I did
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: 'mason@test.com',
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: 'jason@test.com',
phone: '7856452187',
addresses: [
{
street: 'Street 5',
zipCode: '23547',
city: 'Utah',
},
{
street: 'Street 5',
zipCode: '23547',
city: 'Ohio',
},
],
},
];