【问题标题】:Angular 2+: Accessing Reactive Forms independently with Validation in ngFor loopAngular 2+:使用 ngFor 循环中的验证独立访问反应式表单
【发布时间】:2020-05-11 13:21:52
【问题描述】:

我正在使用响应式表单。我有两组数据。它们是:

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

这是我想要做的:

  1. 在 UI 中,我循环显示项目并显示项目 ID 和项目名称。工作正常。
  2. 我正在循环浏览组并在选择框中显示组 ID 和组名称。也很好用。
  3. 我将上面第 2 点的 Select 框放在上面第 1 点的 Items 的 ngFor 循环中,以便每个 Item 都可以分配给一个 Group。假设我单击第一个选择框,目的是从列表中为第一个项目分配一个组。然后,我没有单击显示的任何选项,而是单击页面上的其他位置。 此操作也会显示页面上所有其他表单控件/选择框的错误消息。如果我为页面其余部分的任何其他选择框选择一个值,则错误消息为甚至第一个下拉列表也被清除。 这是问题所在,不应该发生这种情况。该错误应该只显示与第一个项目配对的选择框,而不是页面中旨在与其他项目配对的其他选择框。这些错误中的每一个都应独立显示在这些表单中。

问题摘要:

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

问题的现场演示:

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

现场演示编辑器代码:

这里是Live Demo Editor code

【问题讨论】:

  • 你在循环使用 allData,但重复使用同一个 formGroup 实例,这看起来很奇怪
  • 是的,这就是我发布我的问题的原因,因为我不知道在这里要改变什么。

标签: angular angular2-forms angular-reactive-forms ngfor reactive-forms


【解决方案1】:

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

  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>

【讨论】:

  • 我使用您的代码创建了一个演示,但它似乎不起作用。我的意思是,验证部分根本没有被触发。请检查:stackblitz.com/edit/loop-reactive-forms-try-1
  • 以上代码用于单独访问表单控件,它确实有效。如果您需要访问错误,您可以通过使用form.controls[id].errors 访问表单控件本身来访问错误对象或form.controls[id].getError(errorKey) 访问单个错误
  • 我需要单独访问验证/错误,因为这是要求的核心(也许您在标题中错过了它)。您能否分叉您的代码(我在将近 50 分钟前共享的链接)并对您的代码进行必要的更改并与我分享其Editor link,以便我更好地理解您的代码?
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

    实际上,从您的代码角度了解,有一个名为(formControlName)的表单:partnerGroupsId,因此它们都以相同的 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>
    

    【讨论】:

    • 我注意到在您的代码中,myid 的值显示为 Not a Number,即 myid NaN。您能否纠正这一点并分享您的代码的编辑器链接,以便我更好地理解它?此外,您似乎对我的仓库进行了分叉,因此您所做的更改在我的仓库中可见。
    • 给你,我刚刚更新了答案,这是editor link
    • 在这种情况下,如何将partnerGroupsId{{ i }} 的选定选项的值传递给函数assignGroup()
    • 你能用这个&lt;select formControlName="partnerGroupsId{{ i }}" (change)="assignGroup(item.item_id, groupvalue.value)" #groupvalue&gt; &lt;option value=""&gt;Select a Group from this list&lt;/option&gt; &lt;option *ngFor="let partnerGroups of allDataGroups;" value="{{partnerGroups.group_id}}" selected&gt; {{partnerGroups.display_name}} &lt;/option&gt; &lt;/select&gt;替换那个选择元素吗
    • 我已经按照实际结构Check live edit code完成了你想要的函数assignGroup()
    【解决方案4】:

    我查看了多个堆栈溢出标头。在 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;
          }));
      }
    
    
    }
    
    

    截图

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-04-01
      • 2017-04-12
      • 2019-06-28
      • 2017-09-09
      • 1970-01-01
      • 2019-05-04
      相关资源
      最近更新 更多