Angular 2+:在 ngFor 循环中使用验证独立访问反应式表单

Angular 2+: Accessing Reactive Forms independently with Validation in ngFor loop

我正在使用 Reactive 表单。我有2组数据。他们是:

  1. 项目列表 - 包含项目 ID 和项目名称
  2. 群组列表 - 包含群组 ID 和群组名称

这是我想要做的:

  1. 在 UI 中,我循环遍历项目并显示项目 ID 和项目名称。工作正常。
  2. 我循环浏览组并在 Select 框中显示组 ID 和组名。也很好用。
  3. 我将上面第 2 点中的 Select 框放在上面第 1 点中的 Items 的 ngFor 循环中,这样 每个 Item 都可以分配给一个 Group.假设我单击第一个 Select 框,目的是从列表中为第一项分配一个组。然后我没有点击显示的任何选项,而是点击页面上的其他地方。 此操作还会显示页面上所有其他表单 controls/select 框的错误消息。 如果我 select 任何其他 select 的值] 框,然后甚至清除第一个下拉菜单的错误消息。 这就是问题所在,不应发生。该错误应该仅针对与第一项配对的 Select 框显示,而不针对页面中旨在与其他项配对的其他 select 框显示。这些错误中的每一个都应该针对每个表单独立显示。

问题摘要:

我正在尝试为每个项目分配一个组。所以我试图修复表单的(上述)连接行为,以便 每个表单应该在循环中独立于另一个表单工作 并且在 selecting 一个组之后,单击 "Assign Group" 按钮时,我试图仅访问已提交表单的相应项目 ID 和组 ID,而不是其他表单。如何通过在提交的表单被访问时正确显示 Validation/Errors 来实现这一点?

问题的现场演示:

我已经设置了一个 StackBlitz demo here 来复制这个问题。

现场演示编辑代码:

这里是Live Demo Editor code

您在模板中使用的循环正在使用同一个 FormControl 创建多个表单。 有几种方法可以解决这个问题,一种是使用 FormArray,但我认为使用 FormControl id 的 item_id 属性是最简单的方法,您可以使用 form.controls[id][= 访问模板中的控件14=]

  form: FormGroup = new FormGroup({});
  allData = [
            {
                "item_name": "Test 1",
                "item_id": "1",
            },
            {
                "item_name": "Test 2",
                "item_id": "2",
            },
            {
                "item_name": "Test 3",
                "item_id": "3",
            },
            {
                "item_name": "Test 4",
                "item_id": "4",
            },
            {
                "item_name": "Test 5",
                "item_id": "5",
            },
        ];
  allDataGroups = [
            {
                "display_name": "Group 1",
                "group_id": "1",
            },
            {
                "display_name": "Group 2",
                "group_id": "2",
            },
            {
                "display_name": "Group 3",
                "group_id": "3",
            }
        ];

  constructor() {
    this.createFormControls()
  }

  private createFormControls() {
    for (const datum of this.allData) {
      const id = datum.item_id;
      this.form.addControl(id, new FormControl())
    }
  }
<div class="container">
  <form [formGroup]="form">
    <div *ngFor="let datum of allData">
      <span>{{ datum.item_name }} / Item ID #{{ datum.item_id }}</span>
      <select style="display: block" [formControlName]="datum.item_id">
        <option *ngFor="let group of allDataGroups" [value]="group.group_id">
          {{ group.display_name }}
        </option>
      </select>
      <button>Assign Group</button>
    </div>
  </form>
</div>

做一个 for 循环,然后将每个表单放入它自己的组件中。

<div *ngFor="let item of group">
   <custom-form [item]="item"></custom-form>
</div>

在自定义表单中发生的一切都保留在自定义表单中,这就是您想要的。如果您想将结果发送到链上,您当然可以使用 eventEmitter

实际上根据您的代码 angular 了解到有一个名为:partnerGroupsId 的表单 (formControlName) 所以它们都与相同的 ID 和名称相关联

现场解决方案:https://angular-2-accessing-reactive-forms-independently.stackblitz.io

实时编辑代码:https://stackblitz.com/edit/angular-2-accessing-reactive-forms-independently

解决方案:

您需要循环遍历数据 (this.allData) 以构建表单生成器组:

const groups = {};

for(let i = 0; i< this.allData.length; i++) {
  groups['partnerGroupsId' + i] = ['', [
            Validators.required,
  ]];
}

this.applicationForm = this.formBuilder.group(
        groups
);

并使用下面的 HTML :

<ul class="pb-0">
<ng-container *ngFor="let item of allData; let i = index">
    <li>
        <label class="label">
            {{item.item_name}} /
             <span class="label-heading">Item ID # {{item.item_id}}</span>
        </label>
    <form [formGroup]="applicationForm" (ngSubmit)="assignGroup()"
          (keyup.enter)="assignGroup()"
          class="full-width">
          <label position="floating">Select Group</label>
          <select formControlName="partnerGroupsId{{ i }}">
            <option value="">Select a Group from this list</option>
              <option *ngFor="let partnerGroups of allDataGroups;"
                      value="{{partnerGroups.group_id}}" selected>
                  {{partnerGroups.display_name}}
              </option>                  
          </select>
      <div class="validaterrors">
          <ng-container *ngFor="let validation of validationMessages.partnerGroupsId;"   >
              <div class="error-message"
                    *ngIf="applicationForm.get('partnerGroupsId' + i).hasError(validation.type) && (applicationForm.get('partnerGroupsId' + i).dirty || applicationForm.get('partnerGroupsId' + i).touched)">
                  {{ validation.message }}
              </div>
          </ng-container>
      </div>

        <button color="primary" expand="block" type="submit" class="">
            Assign Group
        </button>
    </form>
    </li>
</ng-container>

我查看了多个堆栈溢出 headers。在 Array 中,我根据有多少员工开发了一个动态结构。不使用 FormArray.

的更实用和尖锐的答案

FormArray 示例令人困惑,如果有可用的数据数组,对我来说是不必要的。

我意识到我在我的项目中更实际地使用了类似的结构。希望能帮到大家一点点。

form.component.html

<form [formGroup]="coordinateFormGroup">
    <div class="coordinate-item" *ngFor="let coordinate of coordinates; index as i" fxLayout="row"
        fxLayoutAlign="start center">
        <div class="circle">
            {{ coordinate.label }}
        </div>
        <div class="source-type">
            <mat-form-field>
                <mat-select formControlName="sourceType{{ i + 1 }}"
                    placeholder="Select Soruce Type..."
                    required>
                    <mat-option *ngFor="let source of sources$ | async" [value]="source">
                        {{ source.title }}
                    </mat-option>
                </mat-select>
                <mat-error *ngIf="coordinateControls({ 
                    controlName: 'sourceType', 
                    index: i, 
                    errorName: 'required' })">
                    Required
                </mat-error>
            </mat-form-field>
        </div>

        <div class="add-photo">
            <button mat-icon-button aria-label="take a photo">
                <mat-icon>camera_alt</mat-icon>
            </button>
        </div>

    </div>
    <button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!this.coordinateFormGroup.valid">
        SUBMIT
    </button>
</form>

form.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SourcesQuery, SourcesGQL } from '@generated-types';

interface Coordinate {
  lat: number,
  lng: number
}

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})

export class FormComponent implements OnInit {

  coordinateFormGroup: FormGroup;
  coordinates: Coordinate[] = [];
  sources$: Observable<SourcesQuery['sources']>; // for option select

  constructor(
    private sourcesGQL: SourcesGQL,
    private formBuilder: FormBuilder,
  ) {
  }

  ngOnInit(): void {
    this.getSources(); // async

    // init coordinate form group
    this.coordinateFormGroup = this.formBuilder.group({
      // must be added for each coordinate object.
    });

  }

  // cleaner error caller on the html side, also a shortcut to walk through the index
  coordinateControls({ controlName, index, errorName }: { controlName: string, index: number, errorName: string }): boolean {
    // bring errors for each different formControlName in the loop.
    const name = `${controlName}${index + 1}`;
    return this.coordinateFormGroup.controls[name].hasError(errorName);
  }

  // Data from the coordinate service.
  // The data emitted are reprocessed here in real time with formBuilder.
  // This function works whenever data changes.
  getCoordinateCache(coordinates: Coordinate[]): Coordinate[] {
    this.coordinates = coordinates;

    // we rebuild the form group every time.
    this.coordinateFormGroup = this.formBuilder.group({

    });

    // we add new formControl for each coordinate.
    coordinates.forEach((obj, i) => {
      this.coordinateFormGroup.addControl(`sourceType${i + 1}`, new FormControl('', [Validators.required]));
    });

    console.log(this.coordinateFormGroup);
    return this.coordinates;
  }

  // Data received for option select.
  getSources(): void {
    this.sources$ = this.sourcesGQL
      .watch({ keyword: '', skip: 0, take: 0 })
      .valueChanges.pipe(map(({ data }) => {
        return data.sources;
      }));
  }


}

截图