angulartypescriptangular-materialangular-reactive-formsformarray

Form components of FormArray in mat-table not sorting properly


I have a mat-table with form control components in each row like image below: enter image description here

and the data model like below:

export class ProductClass {
  constructor(
    public id: string,
    public name: string,
    public date: Date,
    public weight: number,
    public input1: string,
    public input2: string
  ) {}
}

Each row of data contains Product ID, Product Name, and Product Date as header, and additional child data which are Weight, Input1, and Input 2.

I populate the table with sample data below:

sample: ProductClass[] =
    [
        new ProductClass('PID1', 'Product x', new Date(), 1, 'a', '1'),
        new ProductClass('PID2', 'Product g', new Date(), 2, 'b', '2'),
        new ProductClass('PID3', 'Product s', new Date(), 3, 'c', '3'),
        new ProductClass('PID4', 'Product j', new Date(), 4, 'a', '4'),
        new ProductClass('PID5', 'Product r', new Date(), 5, 'b', '5'),
        new ProductClass('PID6', 'Product w', new Date(), 6, 'c', '6'),
        new ProductClass('PID7', 'Product k', new Date(), 7, 'a', '7'),
    ];

I am using mat-sort-header to enable the table sorting, but I realize when perform the sort, all data with static text will sort accordingly, except the form control component which are input1 and input2. For example, base on the sample data above, I have the input1 value as "a,b,c,a,b,c,a", so if I sort the table with Product ID descending order, the input1 value suppose to be in order of "a,c,b,a,c,b,a", but it is not. After the sort, input1 value is in the order of "a,b,b,a,c,c,a", which just doesn't make sense. But if I put the value of input1 as static text like {{child.value.input1}}, it will display the correct value

Template

<form [formGroup]="productForm">
  <main>
    <section class="container-fluid">
      <table mat-table formArrayName="productsArray" [dataSource]="tableDetails" multiTemplateDataRows
        class="maTable maTable-bordered" matSort>

        <ng-container matColumnDef="id">
          <mat-header-cell *matHeaderCellDef mat-sort-header> Product ID </mat-header-cell>
          <mat-cell *matCellDef="let row"> {{row.value.id}} </mat-cell>
        </ng-container>

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

        <ng-container matColumnDef="date">
          <mat-header-cell *matHeaderCellDef mat-sort-header> Product Date </mat-header-cell>
          <mat-cell *matCellDef="let row"> {{row.value.date | date:'dd-MMM-yyyy'}} </mat-cell>
        </ng-container>

        <ng-container matColumnDef="expandedDetail">
          <mat-cell *matCellDef="let child ; let rowindex = dataIndex" [attr.colspan]="tableColumns.length"
            [formGroupName]="getActualIndex(rowindex)">
            <div class="col-sm-6 mt-3">
              <div class="row">
                <h6>Weight: </h6>
                <p class="rounded-sm"> {{child.value.weight}}KG </p>
              </div>
              <div class="row">
                <h6>Input 1</h6><br/>
                <input type="radio" formControlName="input1" value="a">
                <label for="a">A</label>

                <input type="radio" formControlName="input1" value="b">
                <label for="b">B</label>

                <input type="radio" formControlName="input1" value="c">
                <label for="c">C</label>
              </div>
              {{child.value.input1}}

              <div class="row pb-1">
                <h6>Input 2</h6>
                <textarea formControlName="input2" class="rounded-sm border-light-gray px-4" aria-label="Remark" maxlength="500"></textarea>
                {{child.value.input2}}
              </div>
            </div>
          </mat-cell>
        </ng-container>

        <mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
        <mat-row matRipple *matRowDef="let child; columns: tableColumns;" class="element-row"></mat-row>
        <mat-row *matRowDef="let row ; columns: ['expandedDetail'];" style="overflow: hidden"></mat-row>
      </table>
      <mat-paginator [pageSizeOptions]="[5, 10, 25, 100]"></mat-paginator>
    </section>
  </main>
</form>

Component

import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { FormBuilder, FormGroup, AbstractControl } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit {
  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  search: string = '';
  tableColumns: string[] = ['id', 'name', 'date'];
  tableDetails: MatTableDataSource<AbstractControl>;
  productArray = this.fb.array([]);
  productForm = this.fb.group({
    productsArray: this.productArray
  });
  sample: ProductClass[] = [
    new ProductClass('PID1', 'Product x', new Date(), 1, 'a', '1'),
    new ProductClass('PID2', 'Product g', new Date(), 2, 'b', '2'),
    new ProductClass('PID3', 'Product s', new Date(), 3, 'c', '3'),
    new ProductClass('PID4', 'Product j', new Date(), 4, 'a', '4'),
    new ProductClass('PID5', 'Product r', new Date(), 5, 'b', '5'),
    new ProductClass('PID6', 'Product w', new Date(), 6, 'c', '6'),
    new ProductClass('PID7', 'Product k', new Date(), 7, 'a', '7')
  ];

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    for (const product of this.sample) {
      this.productArray.push(
        this.fb.group({
          id: product.id,
          name: product.name,
          date: product.date,
          weight: product.weight,
          input1: product.input1,
          input2: product.input2
        })
      );
    }
  }

  protected init(): void {
    this.tableDetails = new MatTableDataSource();
    this.tableDetails.data = this.productArray.controls;
    this.tableDetails.paginator = this.paginator;
    this.tableDetails.sort = this.sort;

    this.tableDetails.sortingDataAccessor = (
      item: AbstractControl,
      property
    ) => {
      switch (property) {
        case 'date':
          return new Date(item.value.date);
        default:
          return item.value[property];
      }
    };

    const filterPredicate = this.tableDetails.filterPredicate;
    this.tableDetails.filterPredicate = (data: AbstractControl, filter) => {
      return filterPredicate.call(this.tableDetails, data.value, filter);
    };
  }

  ngAfterViewInit(): void {
    this.init();
  }

  applyFilter(): void {
    this.tableDetails.filter = this.search.trim().toLowerCase();
  }

  getActualIndex(index: number): number {
    return index + this.paginator.pageSize * this.paginator.pageIndex;
  }
}

export class ProductClass {
  constructor(
    public id: string,
    public name: string,
    public date: Date,
    public weight: number,
    public input1: string,
    public input2: string
  ) {}
}

I tried to duplicate my code on Stackbitz, although CSS doesn't work, but it did replicate the issue I described above.

Update

I notice that the problem only exists when data is spread across multiple pages of pagination. For example, my pagination size is 5 rows per page. If there is more data than can fit within these 5 rows, then the problem arises.

If I change the pagination size to be larger than the amount of data being displayed, so that all the rows can be rendered on a single page, then the problem does not occur.

Any help would be greatly appreciated!


Solution

  • Turn out it is the problem with FormGroupName, you can't set your FormGroupName with the dataIndex as when you sort the table with mat-sort-header, it only sort the table on UI layer, didn't change the actual order of data set (the order of tableDetails.data)

    So instead of [formGroupName]="getActualIndex(rowindex)", I change it to [formGroupName]="tableDetails.data.indexOf(child)".