Angular - Material Table 的 FormArray 和数据行不正确

Angular - FormArray with Material Table and data row was incorrect

我正在尝试使用 FormArray 和 Angular Material table 创建动态表单。

模板:

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

分量:

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

当我单击编辑(铅笔)按钮编辑我单击的行中的数据时,数据始终填充行号 1,即使按下“添加”按钮到新行也是如此。

我在 Stackblitz

上的代码片段

此行从未重复。因此 rowIndex 将是 并且它 只生成一个 FormGroup 并且第一条记录在 FormArray .

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

解决方案

相反,您必须从 mat-cell 获取 index 以便它迭代生成每个 FormGroup.

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

完成的<form>+mat-table个元素应该如下:

<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