Search code examples
angulartypescriptangular-material

Material Table - Add Custom Column that contains inputs to a FormArray


I have a material table that updates its data using filters. One filter pulls records for invoices that are unpaid. For unpaid records, I've added a custom column that contains an input called payment_amount. The payment_amount field is not part of the table's DataSource object. I don't want it to be because I'm using the input to calculate two other fields in the DataSource. So the process is when a check comes in, I can input the payment amount, then rate and fuel_surchage are automatically calculated sending the updated record to the backend.

The problem is the inputs need to be in a FormArray so they have unique references for Validation and values correspond to the record's id.

I don't know where to start adding these input elements to a Form array. I know I need a FormGroup but I don't know where to place it, and I know I need to declare a FormArray but I'm not sure how to populate it as the table is rendered. Any Ideas?

Here's what I have thus far.

<ng-container matColumnDef="payment_amount">
  <th mat-header-cell mat-sort-header *matHeaderCellDef> Payment Amount</th>
  <td mat-cell *matCellDef="let shipment; let index = index">
    <mat-form-field>
      <mat-label>Payment Amount</mat-label>
      <mat-icon inline matPrefix fontIcon="attach_money"></mat-icon>
      <input matInput required minLength="300" (blur)="calculateBill(shipment.id,$event.target.value)">
    </mat-form-field>
  </td>
</ng-container>

<ng-container matColumnDef="shipments.rate">
   <th mat-header-cell mat-sort-header *matHeaderCellDef> Rate</th>
    <td mat-cell *matCellDef="let shipment"> {{shipment.rate | currency}}</td>
</ng-container>

<ng-container matColumnDef="shipments.fuel_surcharge">
  <th mat-header-cell mat-sort-header *matHeaderCellDef> Fuel Surcharge</th>
  <td mat-cell *matCellDef="let shipment"> {{shipment.fuel_surcharge | currency}}</td>
</ng-container>

In my component;

calculateBill(shipment_id, value){
  //calculates the two fields and updates the table cells. Updating the table view works
  let index = this.dataSource.shipmentsResponse.data.findIndex({
    shipment => shipment.id===shipment_id
  });
  this.dataSource.shipments[index].fuel_surcharge = value-300;
  this.dataSource.shipments[index].rate = 300;
}

Solution

  • You need to use formArrayName="array" on the table and then create a form control for each row on the table, please find below a working stackblitz

    html

    <form [formGroup]="formGroup">
      <table
        mat-table
        [dataSource]="dataSource"
        class="mat-elevation-z8"
        formArrayName="array"
      >
        <ng-container matColumnDef="payment_amount">
          <th mat-header-cell mat-sort-header *matHeaderCellDef>Payment Amount</th>
          <td mat-cell *matCellDef="let shipment; let index = index">
            <mat-form-field>
              <mat-label>Payment Amount</mat-label>
              <mat-icon inline matPrefix fontIcon="attach_money"></mat-icon>
              <input
                matInput
                [formControlName]="index"
                (blur)="calculateBill(shipment.id,$any($event!.target)!.value!)"
              />
            </mat-form-field>
          </td>
        </ng-container>
    
        <ng-container matColumnDef="rate">
          <th mat-header-cell mat-sort-header *matHeaderCellDef>Rate</th>
          <td mat-cell *matCellDef="let shipment">{{shipment.rate | currency}}</td>
        </ng-container>
    
        <ng-container matColumnDef="fuel_surcharge">
          <th mat-header-cell mat-sort-header *matHeaderCellDef>Fuel Surcharge</th>
          <td mat-cell *matCellDef="let shipment">
            {{shipment.fuel_surcharge | currency}}
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="displayColumns"></tr>
        <tr
          mat-row
          *matRowDef="let row; let i = index; columns: displayColumns;"
        ></tr>
      </table>
    </form>
    
    <!-- Copyright 2023 Google LLC. All Rights Reserved.
        Use of this source code is governed by an MIT-style license that
        can be found in the LICENSE file at https://angular.io/license -->
    
    {{formGroup.value | json}}
    

    ts

    import { CommonModule } from '@angular/common';
    import { Component } from '@angular/core';
    import { MatCommonModule } from '@angular/material/core';
    import { MatTableModule } from '@angular/material/table';
    import { MatIconModule } from '@angular/material/icon';
    import { MatFormFieldModule } from '@angular/material/form-field';
    import {
      FormArray,
      FormControl,
      FormGroup,
      ReactiveFormsModule,
      Validators,
    } from '@angular/forms';
    import { MatInputModule } from '@angular/material/input';
    
    export interface PeriodicElement {
      name: string;
      position: number;
      weight: number;
      symbol: string;
    }
    
    const ELEMENT_DATA: any[] = [
      { id: 1, name: 'Hydrogen', payment_amount: 0, rate: 0, fuel_surcharge: 0 },
      { id: 2, name: 'Helium', payment_amount: 0, rate: 0, fuel_surcharge: 0 },
      { id: 3, name: 'Lithium', payment_amount: 0, rate: 0, fuel_surcharge: 0 },
      { id: 4, name: 'Beryllium', payment_amount: 0, rate: 0, fuel_surcharge: 0 },
    ];
    
    /**
     * @title Basic use of `<table mat-table>`
     */
    @Component({
      selector: 'table-basic-example',
      styleUrls: ['table-basic-example.css'],
      templateUrl: 'table-basic-example.html',
      standalone: true,
      imports: [
        MatTableModule,
        CommonModule,
        MatIconModule,
        MatFormFieldModule,
        ReactiveFormsModule,
        MatInputModule,
      ],
    })
    export class TableBasicExample {
      formGroup: FormGroup = new FormGroup({
        array: new FormArray([]),
      });
      displayColumns = ['payment_amount', 'rate', 'fuel_surcharge'];
      dataSource = ELEMENT_DATA;
    
      ngOnInit() {
        const arrayCtrl: FormArray = <FormArray>this.formGroup.get('array');
    
        this.dataSource.forEach((x: any) => {
          arrayCtrl!.push(
            new FormControl(x.payment_amount, [
              Validators.required,
              Validators.max(300),
            ])
          );
        });
      }
    
      calculateBill(shipment_id: any, value: any) {
        //calculates the two fields and updates the table cells. Updating the table view works
        let index = this.dataSource.findIndex(
          (shipment: any) => shipment.id === shipment_id
        );
        this.dataSource[index].fuel_surcharge = value - 300;
        this.dataSource[index].rate = 300;
      }
    }
    

    stackblitz


    References

    1. form array on angular material table