Angular – FormArray with Material Table and data row was incorrect

Issue

I’m trying to create the dynamic form using FormArray and angular material table.

Template:

<div class="container-fluid col-md-12">
<div class="detail-container mat-elevation-z8" style="border-radius: 5px !important;">
  <div class="d-flex float-md-end" style="padding: 10px;">
    <div class="input-group">
      <button class="btn btn-sm btn-outline-success" (click)="addRow()">
        <i class="bi bi-file-earmark-plus"></i> Add
      </button>
      <button class="btn btn-sm btn-outline-danger" (click)="removeSelectedRow()">
        <i class="bi bi-file-earmark-minus"></i> Remove
      </button>
    </div>
  </div>

  <!-- -- Here to be customized -->
  <form [formGroup]="myFormDetail" id="formDetail">
    <table class="detail-table" mat-table [dataSource]="DetailDS">
      <div formArrayName="tableRowArray">
        <ng-container *ngFor="let column of columns" [matColumnDef]="column.property">
          <div *ngFor="let tableRow of tableRowArray.controls; let rowIndex = index" [formGroupName]="rowIndex">
            <ng-container *ngIf="column.isProperty">

              <th mat-header-cell *matHeaderCellDef>
                <span *ngIf="(column.label !== 'Edit') && (column.label !== 'Select')"> {{ column.label }}</span>
                <span *ngIf="column.label === 'Select'">
                  <mat-checkbox aria-label="Select All"
                                [checked]="isChecked()"
                                [indeterminate]="isIndeterminate()"
                                (change)="$event ? isAllSelected($event) : null"></mat-checkbox>
                </span>
              </th>


              <td mat-cell *matCellDef="let row">
                <div *ngIf="!row.isEdit">
                  <div *ngIf="column.label === 'Select'">
                    <mat-checkbox (click)="$event.stopPropagation()"
                                  (change)="$event ? toggle(row, $event) : null;"
                                  [checked]="exists(row)">
                    </mat-checkbox>
                  </div>
                  <div  *ngIf="column.label === 'Edit'; spanHeader">
                    <div class="input-group">
                      <button class="btn btn-xs btn-outline-success" (click)="row.isEdit = !row.isEdit">
                        <i class="bi bi-pencil"></i>
                      </button>
                      <button class="btn btn-xs btn-outline-danger" (click)="removeRow(row.id)">
                        <i class="bi bi-x"></i>
                      </button>
                    </div>
                  </div>
                  <span #spanHeader>{{ row[column.property] }}</span>
                </div>

                <div [ngSwitch]="dataSchema[column.label]" *ngIf="row.isEdit">
                  <!-- -- Just showing the rowIndex debugging purpose -->
                  <div *ngSwitchCase="'select'"> {{ rowIndex }}
                  </div>

                  <div *ngSwitchCase="'edit'">
                    <div class="input-group">
                      <button class="btn btn-xs btn-outline-success" (click)="row.isEdit = !row.isEdit;">
                        <i class="bi bi-save"></i>
                      </button>
                      <button class="btn btn-xs btn-outline-primary" (click)="row.isEdit = !row.isEdit; removeRow(row.id, row)">
                        <i class="bi bi-arrow-counterclockwise"></i>
                      </button>
                    </div>
                  </div>

                  <!-- <mat-form-field *ngSwitchCase="'currency'">
                    <ng-container [ngTemplateOutlet]="tCurrency"></ng-container>
                  </mat-form-field> -->

                  <mat-form-field *ngSwitchDefault [style.width.%]="inherit">
                    <mat-label>{{ column.label }}</mat-label>
                    <input matInput [formControlName]="column.property"
                    style="width: inherit !important">
                  </mat-form-field>
                </div>
              </td>
            </ng-container>
          </div>
          </ng-container>
      </div>

      <tr mat-header-row *matHeaderRowDef="visibleColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: visibleColumns" ></tr>
    </table>
  </form>
  <pre><small>
    {{ myFormDetail?.value | json }}
  </small>
  </pre>
</div>
</div>

component:

export class ListColumns {
  label?: string;
  property?: string;
  visible?: boolean;
  isProperty?: boolean;
  type?: string;
  inlineEdit?: boolean;
}

const DATA_SCHEMA = {
  id: 'number',
  coa: 'text',
  description: 'text',
  dc: 'text',
  currency: 'currency',
  amount: 'number',
  local_amount: 'number',
  cross_coa: 'text',
  Edit: 'edit',
  Select: 'select',
};

const DATA_DETAIL = [
  { id: 1, coa: '1111', description: 'Uraian', dc: 'D', currency: 'IDR', amount: 12345, local_amount: 12345, cross_coa: '2222' },
  { id: 2, coa: '2222', description: 'Uraian', dc: 'D', currency: 'IDR', amount: 12345, local_amount: 12345, cross_coa: '1111' },
  { id: 3, coa: '3333', description: 'Uraian', dc: 'D', currency: 'IDR', amount: 12345, local_amount: 12345, cross_coa: '4444' },
];

@Component({
  selector: 'app-mattable-reactive',
  templateUrl: './mattable-reactive.component.html',
  styleUrls: ['./mattable-reactive.component.css'],
})
export class MattableReactiveComponent implements OnInit {
  // -- detail Part
  dataSchema = DATA_SCHEMA;
  DetailDS = DATA_DETAIL;
  // DetailDS: any;
  columns: ListColumns[] = [
    { label: 'Select', property: 'select', visible: true, isProperty: true },
    { label: 'GL Accounts', property: 'coa', visible: true, isProperty: true },
    { label: 'Description', property: 'description', visible: true, isProperty: true },
    { label: 'D/C', property: 'dc', visible: true, isProperty: true },
    { label: 'Currency', property: 'currency', visible: true, isProperty: true },
    { label: 'Amount', property: 'amount', visible: true, isProperty: true },
    { label: 'Local Amount', property: 'local_amount', visible: true, isProperty: true },
    { label: 'Cross GL Account', property: 'cross_coa', visible: true, isProperty: true},
    { label: 'Edit', property: 'edit', visible: true, isProperty: true },
  ];
  selection = [];
  myFormDetail: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.createFormDetail();
    this.getDetailRowData();
  }

  // -- Detail Part
  createFormDetail() {
    this.myFormDetail = this.fb.group({
      tableRowArray: this.fb.array([]),
    });
  }

  createTableRow(detailDS): FormGroup {
    return this.fb.group({
      id: new FormControl(detailDS.id),
      coa: new FormControl(detailDS.coa),
      description: new FormControl(detailDS.description),
      dc: new FormControl(detailDS.dc),
      currency: new FormControl(detailDS.currency),
      amount: new FormControl(detailDS.amount),
      local_amount: new FormControl(detailDS.local_amount),
      cross_coa: new FormControl(detailDS.cross_coa),
      edit: new FormControl(detailDS.edit),
      select: new FormControl(detailDS.select),
    });
  }

  getDetailRowData() {
    // const formArray = this.myFormDetail.get('tableRowArray') as FormArray;
    this.DetailDS.map((item) => {
      console.log('ITEM: ' + JSON.stringify(item));
      this.tableRowArray.push(this.createTableRow(item));
    });
    this.myFormDetail.setControl('tableRowArray', this.tableRowArray);

    console.log('221: DetailDS: ' + JSON.stringify(this.DetailDS));
  }

  get tableRowArray(): FormArray {
    return this.myFormDetail.get('tableRowArray') as FormArray;
  }

  get visibleColumns() {
    return this.columns
      .filter((column) => column.visible)
      .map((column) => column.property);
  }

  addRow() {
    const newRow = { id: Math.floor(Date.now()), coa: '', description: '', dc: '', currency: '', amount: 0, local_amount: 0, cross_coa: '', isEdit: true, isNew: true };
    this.DetailDS = [...this.DetailDS, newRow];
    this.tableRowArray.push(this.createTableRow(newRow));
    console.log('273: DetailDS: ' + JSON.stringify(this.DetailDS));
  }

  removeRow(id, row?) {
    console.log('255: Idx: ' + id);
    console.log('256: Row: ' + JSON.stringify(row));
    console.log('265: DetailDS: ' + JSON.stringify(this.DetailDS));

    var remove = row === undefined || row.isNew ? true : row.isEdit;
    if (remove) {
      this.DetailDS = this.DetailDS.filter((u) => u.id !== id);
    }
    console.log('265: DetailDS: ' + JSON.stringify(this.DetailDS));
  }

  removeSelectedRow() {
    this.DetailDS = this.DetailDS.filter((u: any) => !u.selected);
    this.selection = this.selection.filter((u: any) => !u.selected);
  }
}

When I click the edit (pencil) button to edit the data wherever row I clicked, the data always fill with row number 1 even push the "Add" button to a new row.

my snippet code on stackblitz

Solution

This line was never iterated. Hence the rowIndex will be zero and it generates only one FormGroup with the first record in FormArray.

<div *ngFor="let tableRow of tableRowArray.controls; let rowIndex = index" [formGroupName]="rowIndex">
   ...
</div>

Solution

Instead, you have to get the index from mat-cell so that it iterates to generate each FormGroup.

<td mat-cell *matCellDef="let row; let rowIndex = index" [formGroupName]="rowIndex">
  ...
</td>

The completed <form> + mat-table elements should be as below:

<form [formGroup]="myFormDetail" id="formDetail">
  <table class="detail-table" mat-table [dataSource]="DetailDS">
    <div formArrayName="tableRowArray">
      <ng-container *ngFor="let column of columns" [matColumnDef]="column.property">

          <ng-container *ngIf="column.isProperty">

            <th mat-header-cell *matHeaderCellDef>
                
              <span *ngIf="(column.label !== 'Edit') && (column.label !== 'Select')"> {{ column.label }}</span>
              <span *ngIf="column.label === 'Select'">
                <mat-checkbox aria-label="Select All"
                              [checked]="isChecked()"
                              [indeterminate]="isIndeterminate()"
                              (change)="$event ? isAllSelected($event) : null"></mat-checkbox>
              </span>
            </th>


            <td mat-cell *matCellDef="let row; let rowIndex = index" [formGroupName]="rowIndex">
              <div *ngIf="!row.isEdit">
                <div *ngIf="column.label === 'Select'">
                  <mat-checkbox (click)="$event.stopPropagation()"
                                (change)="$event ? toggle(row, $event) : null;"
                                [checked]="exists(row)">
                  </mat-checkbox>
                </div>
                <div  *ngIf="column.label === 'Edit'; spanHeader">
                  <div class="input-group">
                    <button class="btn btn-xs btn-outline-success" (click)="row.isEdit = !row.isEdit">
                      <i class="bi bi-pencil"></i>
                    </button>
                    <button class="btn btn-xs btn-outline-danger" (click)="removeRow(row.id)">
                      <i class="bi bi-x"></i>
                    </button>
                  </div>
                </div>
                <span #spanHeader>{{ row[column.property] }}</span>
              </div>

              <div [ngSwitch]="dataSchema[column.label]" *ngIf="row.isEdit">
                <!-- -- Just showing the rowIndex debugging purpose -->
                <div *ngSwitchCase="'select'"> {{ rowIndex }}
                </div>

                <div *ngSwitchCase="'edit'">
                  <div class="input-group">
                    <button class="btn btn-xs btn-outline-success" (click)="row.isEdit = !row.isEdit;">
                      <i class="bi bi-save"></i>
                    </button>
                    <button class="btn btn-xs btn-outline-primary" (click)="row.isEdit = !row.isEdit; removeRow(row.id, row)">
                      <i class="bi bi-arrow-counterclockwise"></i>
                    </button>
                  </div>
                </div>

                <!-- <mat-form-field *ngSwitchCase="'currency'">
                  <ng-container [ngTemplateOutlet]="tCurrency"></ng-container>
                </mat-form-field> -->

                <mat-form-field *ngSwitchDefault [style.width.%]="inherit">
                  <mat-label>{{ column.label }}</mat-label>
                  <input matInput [formControlName]="column.property"
                    style="width: inherit !important">
                </mat-form-field>
              </div>
            </td>
          </ng-container>

        </ng-container>
    </div>

    <tr mat-header-row *matHeaderRowDef="visibleColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: visibleColumns"></tr>
  </table>
</form>

Sample Solution on StackBlitz

Answered By – Yong Shun

Answer Checked By – Jay B. (AngularFixing Admin)

Leave a Reply

Your email address will not be published.