Angular 2+:在 ngFor 循环中使用验证独立访问反应式表单
Angular 2+: Accessing Reactive Forms independently with Validation in ngFor loop
我正在使用 Reactive 表单。我有2组数据。他们是:
- 项目列表 - 包含项目 ID 和项目名称
- 群组列表 - 包含群组 ID 和群组名称
这是我想要做的:
- 在 UI 中,我循环遍历项目并显示项目 ID 和项目名称。工作正常。
- 我循环浏览组并在 Select 框中显示组 ID 和组名。也很好用。
- 我将上面第 2 点中的 Select 框放在上面第 1 点中的 Items 的 ngFor 循环中,这样 每个 Item 都可以分配给一个 Group.假设我单击第一个 Select 框,目的是从列表中为第一项分配一个组。然后我没有点击显示的任何选项,而是点击页面上的其他地方。 此操作还会显示页面上所有其他表单 controls/select 框的错误消息。 如果我 select 任何其他 select 的值] 框,然后甚至清除第一个下拉菜单的错误消息。 这就是问题所在,不应发生。该错误应该仅针对与第一项配对的 Select 框显示,而不针对页面中旨在与其他项配对的其他 select 框显示。这些错误中的每一个都应该针对每个表单独立显示。
问题摘要:
我正在尝试为每个项目分配一个组。所以我试图修复表单的(上述)连接行为,以便 每个表单应该在循环中独立于另一个表单工作 并且在 selecting 一个组之后,单击 "Assign Group" 按钮时,我试图仅访问已提交表单的相应项目 ID 和组 ID,而不是其他表单。如何通过在提交的表单被访问时正确显示 Validation/Errors 来实现这一点?
问题的现场演示:
我已经设置了一个 StackBlitz demo here 来复制这个问题。
现场演示编辑代码:
您在模板中使用的循环正在使用同一个 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;
}));
}
}
我正在使用 Reactive 表单。我有2组数据。他们是:
- 项目列表 - 包含项目 ID 和项目名称
- 群组列表 - 包含群组 ID 和群组名称
这是我想要做的:
- 在 UI 中,我循环遍历项目并显示项目 ID 和项目名称。工作正常。
- 我循环浏览组并在 Select 框中显示组 ID 和组名。也很好用。
- 我将上面第 2 点中的 Select 框放在上面第 1 点中的 Items 的 ngFor 循环中,这样 每个 Item 都可以分配给一个 Group.假设我单击第一个 Select 框,目的是从列表中为第一项分配一个组。然后我没有点击显示的任何选项,而是点击页面上的其他地方。 此操作还会显示页面上所有其他表单 controls/select 框的错误消息。 如果我 select 任何其他 select 的值] 框,然后甚至清除第一个下拉菜单的错误消息。 这就是问题所在,不应发生。该错误应该仅针对与第一项配对的 Select 框显示,而不针对页面中旨在与其他项配对的其他 select 框显示。这些错误中的每一个都应该针对每个表单独立显示。
问题摘要:
我正在尝试为每个项目分配一个组。所以我试图修复表单的(上述)连接行为,以便 每个表单应该在循环中独立于另一个表单工作 并且在 selecting 一个组之后,单击 "Assign Group" 按钮时,我试图仅访问已提交表单的相应项目 ID 和组 ID,而不是其他表单。如何通过在提交的表单被访问时正确显示 Validation/Errors 来实现这一点?
问题的现场演示:
我已经设置了一个 StackBlitz demo here 来复制这个问题。
现场演示编辑代码:
您在模板中使用的循环正在使用同一个 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;
}));
}
}