Mark
Mark

Reputation: 2587

Angular 7 mat-table with array in array

I'm using Angular 7 and using mat-table. https://material.angular.io/components/table/overview

However, all examples I can find use a simple array to create the mat-table from. My array (see below) has an array in an array. I need to iterate over the Results array and then the "line_items" array. But can't find a single example of how this is done?

Component.ts

<mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChanged($event)">

    <ng-container matColumnDef="line_items">
      <mat-header-cell *matHeaderCellDef mat-sort-header>line_items</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.line_items}}</mat-cell>
    </ng-container>

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

    <ng-container matColumnDef="number">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Number</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.number}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="category">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Category</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.category}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="original_budget">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Original Budget</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.original_budget}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="approved_cos">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Approved Cos</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.approved_cos}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="revised_budget">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Revised Budget</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.revised_budget}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="pending_budget_changes">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Pending Budget Changes</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.pending_budget_changes}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="projected_budget">
      <mat-header-cell *matHeaderCellDef mat-sort-header>Projected Budget</mat-header-cell>
      <mat-cell *matCellDef="let element">{{element.projected_budget}}</mat-cell>
    </ng-container>

    <ng-container matColumnDef="loading">
      <mat-footer-cell *matFooterCellDef colspan="6">
        Loading data...
      </mat-footer-cell>
    </ng-container>

    <ng-container matColumnDef="noData">
      <mat-footer-cell *matFooterCellDef colspan="6">
        No data.
      </mat-footer-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;" (click)="selectRow(row, $event)" (dblclick)="onEdit(row, $event)"></mat-row>

    <mat-footer-row *matFooterRowDef="['loading']" [ngClass]="{'hide':dataSource!=null}"></mat-footer-row>
    <mat-footer-row *matFooterRowDef="['noData']" [ngClass]="{'hide':!(dataSource!=null && dataSource.data.length==0)}"></mat-footer-row>
</mat-table>

Array

{
  "results": [
    {
      "name": "General Requirements",
      "id": "1",
      "line_items": [
        {
          "name": "Project Manager",
          "number": "442",
          "category": "L",
          "original_budget": 30000,
          "approved_cos": 0,
          "revised_budget": 30000,
          "pending_budget_changes": 0,
          "projected_budget": 30000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 30000,
          "estimated_cost_at_completion": 30000,
          "projected_over_under": 0
        },
        {
          "name": "Project Engineer",
          "number": "564",
          "category": "L",
          "original_budget": 17000,
          "approved_cos": 0,
          "revised_budget": 17000,
          "pending_budget_changes": 0,
          "projected_budget": 17000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 17000,
          "estimated_cost_at_completion": 17000,
          "projected_over_under": 0
        },
        {
          "name": "Superintendent",
          "number": "766",
          "category": "L",
          "original_budget": 55000,
          "approved_cos": 0,
          "revised_budget": 55000,
          "pending_budget_changes": 0,
          "projected_budget": 55000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 55000,
          "estimated_cost_at_completion": 55000,
          "projected_over_under": 0
        },
        {
          "name": "Project Coordinator",
          "number": "876",
          "category": "L",
          "original_budget": 5000,
          "approved_cos": 0,
          "revised_budget": 5000,
          "pending_budget_changes": 0,
          "projected_budget": 5000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 5000,
          "estimated_cost_at_completion": 5000,
          "projected_over_under": 0
        }
      ]
    },
    {
      "name": "Doors And Windows",
      "id": "2",
      "line_items": [
        {
          "name": "Doors",
          "number": "600",
          "category": "O",
          "original_budget": 2000,
          "approved_cos": 0,
          "revised_budget": 2000,
          "pending_budget_changes": 0,
          "projected_budget": 2000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 2000,
          "estimated_cost_at_completion": 2000,
          "projected_over_under": 0
        },
        {
          "name": "Doors",
          "number": "600",
          "category": "S",
          "original_budget": 67000,
          "approved_cos": 0,
          "revised_budget": 67000,
          "pending_budget_changes": 0,
          "projected_budget": 67000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 67000,
          "estimated_cost_at_completion": 67000,
          "projected_over_under": 0
        }
      ]
    },
    {
      "name": "Finishes",
      "id": "3",
      "line_items": [
        {
          "name": "Kitchen Sink",
          "number": "800",
          "category": "O",
          "original_budget": 5000,
          "approved_cos": 0,
          "revised_budget": 5000,
          "pending_budget_changes": 0,
          "projected_budget": 5000,
          "committed_costs": 0,
          "direct_costs": 0,
          "pending_costs_changes": 0,
          "projected_costs": 0,
          "forecast_to_complete": 5000,
          "estimated_cost_at_completion": 5000,
          "projected_over_under": 0
        }
      ]
    }


  ]
}

UPDATE

I tried the below, hoping it would work, but nope...

<ng-container *ngFor="let budget of budgets">

    <ng-container *ngFor="let item of budget.line_items">

      {{item | json}}

  <mat-table [dataSource]="item" matSort (matSortChange)="sortChanged($event)">

enter image description here

Upvotes: 4

Views: 5173

Answers (2)

Eliseo
Eliseo

Reputation: 57939

Another approach can be use a single table. You create an array like

dataExpanded=this.data.results.reduce((a:any,b:any)=>{
     return a.concat({name:b.name,id:b.id,number: "",category: "O",original_budget: 0,approved_cos: 0,revised_budget:0,pending_budget_changes: 0,projected_budget: 0,committed_costs: 0,direct_costs: 0,pending_costs_changes: 0,projected_costs: 0,forecast_to_complete: 0,estimated_cost_at_completion: 0,projected_over_under:0
    },...b.line_items.map((x:any)=>({id:0,...x})))
  },[])

And a table like

<table mat-table [dataSource]="dataExpanded" class="mat-elevation-z8">
  <ng-container *ngFor="let column of displayedColumns;let i=index" [matColumnDef]="column">
    <th mat-header-cell *matHeaderCellDef>{{header[i]}}</th>
    <ng-container *matCellDef="let element">
    <td class="header" mat-cell [attr.colspan]="15" *ngIf="element.id && column=='name'">
      {{element.name}}
    </td>
    <ng-container *ngIf="!element.id">
      <td *ngIf="column!='name'" mat-cell > {{element[column]}} </td>
      <td *ngIf="column=='name'" mat-cell > {{element.number}} - {{element.name}} </td>
    </ng-container>
    </ng-container>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

See stackblitz

Upvotes: 1

SELA
SELA

Reputation: 6813

Based on the information provided I assume you are trying to have Mat-Table and a Nested Mat-Table for each of the rows in the parent table. This can be archived below. I have created a simple stackblitz sample with small data set similar to yours. In provided sample data set by myself addresses refer to your examples line_items attribute.

Please find the working sample stackblitz here.

Please find the code below :

component.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'];
  expandedElement: User | 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]);
  }

  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;
}

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"
      },
      {
        street: "Street 2",
        zipCode: "78554",
        city: "Texas"
      }
    ]
  },
  {
    name: "Eugene",
    email: "[email protected]",
    phone: "8786541234",
  },
  {
    name: "Jason",
    email: "[email protected]",
    phone: "7856452187",
    addresses: [
      {
        street: "Street 5",
        zipCode: "23547",
        city: "Utah"
      },
      {
        street: "Street 5",
        zipCode: "23547",
        city: "Ohio"
      }
    ]
  }
];

component.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>

    <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"></tr>
</table>

Upvotes: 1

Related Questions