如何在 table 和 angular 中使用嵌套表单数组以反应形式执行验证?
How to perform validation in reactive form with nested form arrays in table with angular?
Stackblitz link:https://stackblitz.com/edit/angular-ivy-bafyye?file=src/app/components/user-details/user-details.component.ts
我为用户的汽车详细信息创建了嵌套反应式表单,如下所示:
用户-details.component.ts
export interface User {
name: string;
car: Cars[];
}
export interface Cars {
id: Number;
company: CarCompany;
model: CarModel;
parts: CarPartName[];
registrationAndBillingDate: RegistrationAndBillingDate[];
}
export interface RegistrationAndBillingDate {
id: Number;
registrationDate: Date;
billingDate: Date;
}
export class CarCompany {
id: number;
name: string;
}
export class CarModel {
id: number;
name: string;
}
export class CarPartName {
id: number;
name: string;
}
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.css'],
})
export class UserDetailsComponent implements OnInit {
userDetailsForm: FormGroup;
submitted = false;
company: CarCompany[] = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Ferrari' },
{ id: 3, name: 'Toyota' },
];
model: CarModel[] = [
{ id: 1, name: 'SUV' },
{ id: 2, name: 'SEDAN' },
];
partName: CarPartName[] = [
{ id: 1, name: 'WHEELS' },
{ id: 2, name: 'FILTERS' },
];
registrationDate: Date;
expiryDate: Date;
userDetailsFormJson: any;
constructor(private fb: FormBuilder) {
this.userDetailsForm = new FormGroup({});
}
ngOnInit() {
this.createUserDetailsForm();
}
createUserDetailsForm() {
this.userDetailsForm = this.fb.group({
name: [null, Validators.required],
cars: this.fb.array([this.createCarsForm()]),
});
}
//car form
createCarsForm(): FormGroup {
return this.fb.group({
carCompany: this.createCarCompnayForm(),
carModel: this.createCarModelForm(),
carParts: this.fb.array([this.createCarPartsForm()]),
carRegistartaionAndBillingDate: new FormArray([
this.createRegistrationAndBillingDateForm(),
]),
});
}
createCarCompnayForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
createCarModelForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, Validators.required),
});
}
//form creation for car parts
createCarPartsForm(): FormGroup {
return this.fb.group({
partName: this.createCarPartNameForm(),
available: new FormControl(null),
});
}
createCarPartNameForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
//form creation for registration and billing cycle
createRegistrationAndBillingDateForm() {
return this.fb.group({
registrationDate: new FormControl(null, Validators.required),
billingDate: new FormControl(null, Validators.required),
});
}
get form() {
return this.userDetailsForm.controls;
}
get cars() {
return this.userDetailsForm.get('cars') as FormArray;
}
addCars() {
this.cars.push(this.createCarsForm());
}
removeCars(k: Required<number>) {
this.cars.removeAt(k);
}
getRegistrationAndBillingDate(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).controls;
}
addRegistrationAndBillingDate(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).push(this.createRegistrationAndBillingDateForm());
}
removeRegistrationAndBillingDate(index, j: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).removeAt(j);
}
addParts(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).push(this.createCarPartsForm());
}
getPartsForm(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).controls;
}
removeParts(index, l: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).removeAt(l);
}
onSubmit() {
this.submitted = true;
this.userDetailsFormJson = this.userDetailsForm.getRawValue();
}
}
用户-details.component.html(ui部分)
<div>
<div>
<div>
<div [formGroup]="userDetailsForm">
<fieldset>
<legend>User Details</legend>
<div>
<table>
<tr></tr>
<tr>
<td>
<p>Name</p>
</td>
<td>
<input
size="35"
type="text"
formControlName="name"
style="width: min-content"
placeholder="enter user name"
/>
</td>
</tr>
</table>
</div>
</fieldset>
<fieldset>
<legend>Cars</legend>
<button class="btn btn-outline-primary" (click)="addCars()">
Add New Car
</button>
<ng-container formArrayName="cars">
<div class="row mt-2">
<div class="table-responsive">
<table class="table-bordered table_car">
<thead>
<tr>
<th>Company</th>
<th>Model</th>
<th>Registration and Billing</th>
<th>Parts</th>
<th></th>
</tr>
</thead>
<tbody *ngFor="let o of cars.controls; let k = index">
<tr class="table_car-tr" [formGroupName]="k">
<td>
<div formGroupName="carCompany">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Company
</option>
<option
*ngFor="let comp of company"
[ngValue]="comp.id"
>
{{ comp.name }}
</option>
</select>
</div>
</td>
<td>
<div formGroupName="carModel">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Model
</option>
<option
*ngFor="let mod of model"
[ngValue]="mod.id"
>
{{ mod.name }}
</option>
</select>
</div>
</td>
<td>
<table
class="table-responsive exp"
style="display: block"
formArrayName="carRegistartaionAndBillingDate"
>
<thead>
<tr>
<th>Registration Date</th>
<th>Billing Date</th>
<th>
<button
style="height: 24px"
(click)="addRegistrationAndBillingDate(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="
let reg of getRegistrationAndBillingDate(k);
let j = index
"
>
<tr
class="registration_and_billing_date-table-tr"
[formGroupName]="j"
>
<td>
<input
formControlName="registrationDate"
type="date"
/>
</td>
<td>
<input
formControlName="billingDate"
type="date"
/>
</td>
<td>
<button
style="height: 24px"
(click)="
removeRegistrationAndBillingDate(k, j)
"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<table class="table table-sm" formArrayName="carParts">
<thead>
<tr>
<th>Part Name</th>
<th>Available</th>
<th>
<button
style="height: 24px"
(click)="addParts(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="let part of getPartsForm(k); let l = index"
>
<tr [formGroupName]="l">
<td>
<div formGroupName="partName">
<select formControlName="id">
<option [ngValue]="null" disabled>
Select Part
</option>
<option
*ngFor="let pName of partName"
[ngValue]="pName.id"
>
{{ pName.name }}
</option>
</select>
</div>
</td>
<td>
<input
type="checkbox"
id="licensed"
formControlName="available"
(value)="(true)"
selected="true"
/>
</td>
<td>
<button
style="height: 24px"
(click)="removeParts(k, l)"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<button style="height: 24px" (click)="removeCars(k)">
x
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container>
</fieldset>
</div>
<div class="card-footer text-center">
<button (click)="onSubmit()" class="btn btn-primary">Submit</button>
</div>
</div>
<div class="card mt-2">
<div class="card-header">Result</div>
<div class="card-body">
<code>
<pre>
{{ userDetailsFormJson | json }}
</pre>
</code>
</div>
</div>
</div>
</div>
我还在表单末尾包含了 json 输出,这样可以更容易理解表单数据的结构。
JSON结构如下:
{
"name": "User",
"cars": [{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
},
{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
}
]
}
我想在汽车 table 中对公司和车型进行验证,这样一个公司可以连续拥有一个车型。如果同一辆车在下一行也有相同的型号,那么该行需要显示消息,说明重复值或类似内容。
示例:如果汽车 table 的公司值为福特,车型值为 SUV,如果再次排在第二行,如果汽车公司值为福特,车型值为 SUV,则需要重复。
我还想验证日期部分以及注册日期应小于开票日期的部分。如果账单日期小于注册日期需要抛出验证错误。我已经在 html 表格中尝试过,如下所示:
<div style="color: red;font-size: 10px;margin-left: 10px;text-align: center;"
*ngIf=" reg.controls.registrationDate.value >reg.controls.billingDate.value">
Invalid Billing Date
</div>
有更好的方法吗?而且我还想在 Part column car table 的 Part Name 列的 table 中进行验证。如果重复零件名称,我想抛出验证文本。例如:我将 Wheels 和 Filter 作为零件名称。如果 Wheels 已经存在并且如果用户再次选择 wheels 验证应该被启动。我无法弄清楚应该如何正确完成验证。所以任何一种解决方案或建议都会很棒。
可以使用自定义验证器处理暗示多个组件的验证,我将把它放在 FormArray
A validator 是一个 function 需要一个 AbstractControl
(通常是一个 FormControl
但在这里,如果出现问题,它将是 FormArray
) 并且 return 是 ValidationErrors
,如果一切正常,它将是 null
。
ValidationErrors
是您想要的任何 key/value 对象,因此您可以传递有关约束违规的信息。例如,它可以是:
{
minLength: 2,
maxLength: 20
}
您可以通过 formControl.errors
/ formArray.errors
获取错误。通常,您希望使用验证器的名称作为键,并使用有关约束违规的一些详细信息作为值。
这里是一个实现的命题:
- 将
FormArray
映射到模型的 ID
- 筛选
null
个元素
- 将其映射到
Set
(本质上会删除重复项)并将其大小与数组的大小进行比较
- 如果大小不匹配,有一些重复,return一个
ValidationErrors
duplicateCarValidator(control: AbstractControl): ValidationErrors {
const modelIds = control.value
.map((car) => car.carModel?.id)
.filter((id) => id !== null && id !== undefined);
if (new Set(modelIds).size !== modelIds.length) {
return {
duplicates: true,
};
} else {
return null;
}
}
并像添加任何验证器一样添加它:
cars: this.fb.array(
[this.createCarsForm()],
[this.duplicateCarValidator]
),
这是一个 StackBlitz,添加了一些 console.log
:here
对于注册日期和账单日期,这是另一个验证器,它隐含了 2 个字段,因此可以由另一个自定义验证器处理。
你可以决定放置它:
- 在 2 个字段之一 (registration/billing)
- 上车
FormGroup
逻辑保持不变
Stackblitz link:https://stackblitz.com/edit/angular-ivy-bafyye?file=src/app/components/user-details/user-details.component.ts
我为用户的汽车详细信息创建了嵌套反应式表单,如下所示:
用户-details.component.ts
export interface User {
name: string;
car: Cars[];
}
export interface Cars {
id: Number;
company: CarCompany;
model: CarModel;
parts: CarPartName[];
registrationAndBillingDate: RegistrationAndBillingDate[];
}
export interface RegistrationAndBillingDate {
id: Number;
registrationDate: Date;
billingDate: Date;
}
export class CarCompany {
id: number;
name: string;
}
export class CarModel {
id: number;
name: string;
}
export class CarPartName {
id: number;
name: string;
}
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.css'],
})
export class UserDetailsComponent implements OnInit {
userDetailsForm: FormGroup;
submitted = false;
company: CarCompany[] = [
{ id: 1, name: 'Ford' },
{ id: 2, name: 'Ferrari' },
{ id: 3, name: 'Toyota' },
];
model: CarModel[] = [
{ id: 1, name: 'SUV' },
{ id: 2, name: 'SEDAN' },
];
partName: CarPartName[] = [
{ id: 1, name: 'WHEELS' },
{ id: 2, name: 'FILTERS' },
];
registrationDate: Date;
expiryDate: Date;
userDetailsFormJson: any;
constructor(private fb: FormBuilder) {
this.userDetailsForm = new FormGroup({});
}
ngOnInit() {
this.createUserDetailsForm();
}
createUserDetailsForm() {
this.userDetailsForm = this.fb.group({
name: [null, Validators.required],
cars: this.fb.array([this.createCarsForm()]),
});
}
//car form
createCarsForm(): FormGroup {
return this.fb.group({
carCompany: this.createCarCompnayForm(),
carModel: this.createCarModelForm(),
carParts: this.fb.array([this.createCarPartsForm()]),
carRegistartaionAndBillingDate: new FormArray([
this.createRegistrationAndBillingDateForm(),
]),
});
}
createCarCompnayForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
createCarModelForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, Validators.required),
});
}
//form creation for car parts
createCarPartsForm(): FormGroup {
return this.fb.group({
partName: this.createCarPartNameForm(),
available: new FormControl(null),
});
}
createCarPartNameForm(): FormGroup {
return this.fb.group({
id: new FormControl(null, [Validators.required]),
});
}
//form creation for registration and billing cycle
createRegistrationAndBillingDateForm() {
return this.fb.group({
registrationDate: new FormControl(null, Validators.required),
billingDate: new FormControl(null, Validators.required),
});
}
get form() {
return this.userDetailsForm.controls;
}
get cars() {
return this.userDetailsForm.get('cars') as FormArray;
}
addCars() {
this.cars.push(this.createCarsForm());
}
removeCars(k: Required<number>) {
this.cars.removeAt(k);
}
getRegistrationAndBillingDate(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).controls;
}
addRegistrationAndBillingDate(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).push(this.createRegistrationAndBillingDateForm());
}
removeRegistrationAndBillingDate(index, j: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carRegistartaionAndBillingDate'
)
)).removeAt(j);
}
addParts(index) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).push(this.createCarPartsForm());
}
getPartsForm(index) {
return (<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).controls;
}
removeParts(index, l: Required<number>) {
(<FormArray>(
(<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
'carParts'
)
)).removeAt(l);
}
onSubmit() {
this.submitted = true;
this.userDetailsFormJson = this.userDetailsForm.getRawValue();
}
}
用户-details.component.html(ui部分)
<div>
<div>
<div>
<div [formGroup]="userDetailsForm">
<fieldset>
<legend>User Details</legend>
<div>
<table>
<tr></tr>
<tr>
<td>
<p>Name</p>
</td>
<td>
<input
size="35"
type="text"
formControlName="name"
style="width: min-content"
placeholder="enter user name"
/>
</td>
</tr>
</table>
</div>
</fieldset>
<fieldset>
<legend>Cars</legend>
<button class="btn btn-outline-primary" (click)="addCars()">
Add New Car
</button>
<ng-container formArrayName="cars">
<div class="row mt-2">
<div class="table-responsive">
<table class="table-bordered table_car">
<thead>
<tr>
<th>Company</th>
<th>Model</th>
<th>Registration and Billing</th>
<th>Parts</th>
<th></th>
</tr>
</thead>
<tbody *ngFor="let o of cars.controls; let k = index">
<tr class="table_car-tr" [formGroupName]="k">
<td>
<div formGroupName="carCompany">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Company
</option>
<option
*ngFor="let comp of company"
[ngValue]="comp.id"
>
{{ comp.name }}
</option>
</select>
</div>
</td>
<td>
<div formGroupName="carModel">
<select formControlName="id" required>
<option [ngValue]="null" disabled>
Select Car Model
</option>
<option
*ngFor="let mod of model"
[ngValue]="mod.id"
>
{{ mod.name }}
</option>
</select>
</div>
</td>
<td>
<table
class="table-responsive exp"
style="display: block"
formArrayName="carRegistartaionAndBillingDate"
>
<thead>
<tr>
<th>Registration Date</th>
<th>Billing Date</th>
<th>
<button
style="height: 24px"
(click)="addRegistrationAndBillingDate(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="
let reg of getRegistrationAndBillingDate(k);
let j = index
"
>
<tr
class="registration_and_billing_date-table-tr"
[formGroupName]="j"
>
<td>
<input
formControlName="registrationDate"
type="date"
/>
</td>
<td>
<input
formControlName="billingDate"
type="date"
/>
</td>
<td>
<button
style="height: 24px"
(click)="
removeRegistrationAndBillingDate(k, j)
"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<table class="table table-sm" formArrayName="carParts">
<thead>
<tr>
<th>Part Name</th>
<th>Available</th>
<th>
<button
style="height: 24px"
(click)="addParts(k)"
>
+
</button>
</th>
</tr>
</thead>
<tbody
*ngFor="let part of getPartsForm(k); let l = index"
>
<tr [formGroupName]="l">
<td>
<div formGroupName="partName">
<select formControlName="id">
<option [ngValue]="null" disabled>
Select Part
</option>
<option
*ngFor="let pName of partName"
[ngValue]="pName.id"
>
{{ pName.name }}
</option>
</select>
</div>
</td>
<td>
<input
type="checkbox"
id="licensed"
formControlName="available"
(value)="(true)"
selected="true"
/>
</td>
<td>
<button
style="height: 24px"
(click)="removeParts(k, l)"
>
x
</button>
</td>
</tr>
</tbody>
</table>
</td>
<td>
<button style="height: 24px" (click)="removeCars(k)">
x
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-container>
</fieldset>
</div>
<div class="card-footer text-center">
<button (click)="onSubmit()" class="btn btn-primary">Submit</button>
</div>
</div>
<div class="card mt-2">
<div class="card-header">Result</div>
<div class="card-body">
<code>
<pre>
{{ userDetailsFormJson | json }}
</pre>
</code>
</div>
</div>
</div>
</div>
我还在表单末尾包含了 json 输出,这样可以更容易理解表单数据的结构。
JSON结构如下:
{
"name": "User",
"cars": [{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
},
{
"carCompany": {
"id": 1
},
"carModel": {
"id": 1
},
"carParts": [{
"partName": {
"id": null
},
"available": null
}],
"carRegistartaionAndBillingDate": [{
"registrationDate": null,
"billingDate": null
}]
}
]
}
我想在汽车 table 中对公司和车型进行验证,这样一个公司可以连续拥有一个车型。如果同一辆车在下一行也有相同的型号,那么该行需要显示消息,说明重复值或类似内容。
示例:如果汽车 table 的公司值为福特,车型值为 SUV,如果再次排在第二行,如果汽车公司值为福特,车型值为 SUV,则需要重复。
我还想验证日期部分以及注册日期应小于开票日期的部分。如果账单日期小于注册日期需要抛出验证错误。我已经在 html 表格中尝试过,如下所示:
<div style="color: red;font-size: 10px;margin-left: 10px;text-align: center;"
*ngIf=" reg.controls.registrationDate.value >reg.controls.billingDate.value">
Invalid Billing Date
</div>
有更好的方法吗?而且我还想在 Part column car table 的 Part Name 列的 table 中进行验证。如果重复零件名称,我想抛出验证文本。例如:我将 Wheels 和 Filter 作为零件名称。如果 Wheels 已经存在并且如果用户再次选择 wheels 验证应该被启动。我无法弄清楚应该如何正确完成验证。所以任何一种解决方案或建议都会很棒。
可以使用自定义验证器处理暗示多个组件的验证,我将把它放在 FormArray
A validator 是一个 function 需要一个 AbstractControl
(通常是一个 FormControl
但在这里,如果出现问题,它将是 FormArray
) 并且 return 是 ValidationErrors
,如果一切正常,它将是 null
。
ValidationErrors
是您想要的任何 key/value 对象,因此您可以传递有关约束违规的信息。例如,它可以是:
{
minLength: 2,
maxLength: 20
}
您可以通过 formControl.errors
/ formArray.errors
获取错误。通常,您希望使用验证器的名称作为键,并使用有关约束违规的一些详细信息作为值。
这里是一个实现的命题:
- 将
FormArray
映射到模型的 ID - 筛选
null
个元素 - 将其映射到
Set
(本质上会删除重复项)并将其大小与数组的大小进行比较 - 如果大小不匹配,有一些重复,return一个
ValidationErrors
duplicateCarValidator(control: AbstractControl): ValidationErrors {
const modelIds = control.value
.map((car) => car.carModel?.id)
.filter((id) => id !== null && id !== undefined);
if (new Set(modelIds).size !== modelIds.length) {
return {
duplicates: true,
};
} else {
return null;
}
}
并像添加任何验证器一样添加它:
cars: this.fb.array(
[this.createCarsForm()],
[this.duplicateCarValidator]
),
这是一个 StackBlitz,添加了一些 console.log
:here
对于注册日期和账单日期,这是另一个验证器,它隐含了 2 个字段,因此可以由另一个自定义验证器处理。
你可以决定放置它:
- 在 2 个字段之一 (registration/billing)
- 上车
FormGroup
逻辑保持不变