如何在 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.loghere

对于注册日期和账单日期,这是另一个验证器,它隐含了 2 个字段,因此可以由另一个自定义验证器处理。

你可以决定放置它:

  • 在 2 个字段之一 (registration/billing)
  • 上车FormGroup

逻辑保持不变