Angular - 带有材料表和数据行的 FormArray 不正确

Posted

技术标签:

【中文标题】Angular - 带有材料表和数据行的 FormArray 不正确【英文标题】:Angular - FormArray with Material Table and data row was incorrect 【发布时间】:2021-11-16 11:42:41 【问题描述】:

我正在尝试使用 FormArray 和角度材料表创建动态表单。

模板:

<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上的sn-p代码

【问题讨论】:

【参考方案1】:

此行从未迭代。因此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>

完整的&lt;form&gt; + 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

【讨论】:

我相信您的原始代码没有完成,上面的解决方案刚刚解决了您当前的问题。您必须实现 click 事件的逻辑才能更新表格。 谢谢...就是这样,我需要...接下来解决点击事件。

以上是关于Angular - 带有材料表和数据行的 FormArray 不正确的主要内容,如果未能解决你的问题,请参考以下文章

在角度材料 md 对话框中使用带有所需 ^form 的指令

带有分页的可扩展嵌套 Angular 材料数据表未按预期工作?

Angular 材料 2 在组件之间共享反应形式

Angular 材料 11:具有外观轮廓的 mat-form-field 以在整个项目中具有自定义的边界半径

带有材料设计垫工具栏问题的Angular 6 [重复]

带有 x-www-form-urlencoded 数据的 Angular 6 http 发布请求