如何在 Angular 9 Reactive Form 中获取嵌套的 FormArray 控件

How to get a nested FormArray control in Angular 9 ReactiveForm


编辑

由于上面 Chellappan 的回答,问题的第一部分已经解决,但我遇到了其他问题,这让我做了一个可以在这里找到的 stackblitz: https://stackblitz.com/edit/angular-ivy-bzh3dh

问题是我无法从表单中正确检索 selected 的产品列表,因为它们都被输出了:

  "courses": [
    {
      "courseName": "Dessert",
      "products": [
        {
          "product": true
        },
        {
          "product": false
        },
        {
          "product": null
        }
      ]

我不知道如何能够检索类似的东西:

  "courses": [
    {
      "courseName": "Dessert",
      "products": [
        {
          "Crumble aux pommes": true
        },
        {
          "Mousse au chocolat maison": false
        },
        {
          "Riz au lait": false
        }
      ]

第二个问题是我的复选框的标签不是独立的。如果我添加一个新的 'course' 并且我 select 一些产品,其他课程中所有复选框的标签也会更改。

我希望我的 post 不会太乱,我不会经常在 Stack Overflow 上编辑消息

编辑结束


我有一个带有嵌套形式数组的反应式形式来描述包含多个课程的菜单 每门课程都包含几种产品,但我不知道如何让它发挥作用。 我的实际问题是我无法检索我的二级控件。

这是菜单的 json 结构:

{
  "name": "",
  "description": "",
  "price": "",
  "courses": [
    {
      "courseName": "",
      "products": [
        {
          "product": ""
        }
      ]
    }
  ]
}

我以这个项目为例:

Deep nested reactive form

为了便于阅读,这是我的简化形式:

    <form [formGroup]="menuForm" (ngSubmit)="onSubmit(menuForm)">

        <div class="form-group">
          <input
            class="form-control"
            formControlName="name"
            type="text">
        </div>
        <div class="form-group">
          <textarea
            class="form-control"
            formControlName="description"></textarea>
        </div>
        <div class="col-md-3 form-group">
          <input
            class="form-control"
            formControlName="price"
            type="number">
        </div>
      </div>
      <div class="row col-md-12">
        <button class="btn btn-outline-dark" type="button" (click)="onAddCourse()">
          Add course
        </button>
      </div>

      <div class="row" formArrayName="courses">
        <div *ngFor="let course of getCourses(menuForm); let i = index"
            [formGroupName]="i">

          <div class="...">
              <select
                class="form-control"
                formControlName="courseName">
                <option *ngFor="let course of courses">{{course.name}}</option>
              </select>

              <button type="button" class="mt-2 btn btn-outline-dark" (click)="onLoadProducts(i)">
                Load Products // <- will load products that have course Name selected form the input 'courseName'above
              </button>

              <div class="row" formArrayName="products">
                <div class="col-md-12"
                     *ngFor="let product of getProducts(menuForm); let j = index"
                      [formGroupName]="j">
                  <input class="form-check-input" type="checkbox" id="product{{i}}-{{j}}" 
                   formControlName="product"/>
                  <label class="form-check-label" for="product{{i}}-{{j}}">product</label>
                </div>
              </div>
          </div>
        </div>
      </div>

      <br>
      <div class="row col-md-12">
        <button class="btn btn-success mr-1" type="submit" [disabled]="!menuForm.valid">{{buttonSubmitLabel}}</button>
      </div>
    </form>

这是我在 TS 中的表单初始化:

export class MenuEditComponent implements OnInit {

  menuForm: FormGroup;
  menu: MenuModel;
  categories: CategoryModel[];
...

  constructor(private router: Router,
              private route: ActivatedRoute,
              private productService: ProductService,
              private menuService: MenuService) { }

  ngOnInit(): void {
    this.productService.getCategories().subscribe(
      result => {
        this.categories = result;
      },
      error => {
        console.log(error);
        this.categories = [];
      });
    this.initForm();
  }

  initForm() {
    this.menuForm = new FormGroup({
      name: new FormControl('', [Validators.required, Validators.maxLength(100)]),
      description: new FormControl('', Validators.maxLength(255)),
      price: new FormControl('', [Validators.required, Validators.min(0)]),
      courses: new FormArray([
        this.initCourse(),
      ])
    });
  }

  initCourse() {
    return new FormGroup({
      courseName: new FormControl(''),
      products: new FormArray([
        this.initProduct()
      ])
    });
  }

  initProduct() {
    return new FormGroup({
      product: new FormControl('')
    });
  }

  getCourses(form) {
    return form.controls.courses.controls;
  }

  getProducts(form) {
    return form.controls.products.controls;
  }

  onAddCourse() {
    const control = this.menuForm.get('courses') as FormArray;
    control.push(this.initCourse());
  }

  onLoadProducts(i: number) {
    console.log(this.menuForm.controls.courses);
    const courseControl = this.menuForm.get('courses') as FormArray;
    const course = courseControl.at(i).get('courseName').value;
    console.log(course);
    const products: ProductModel[] = this.mapProducts.get(course);
    console.log(products);
    const productControl = this.menuForm.get('courses').get[i].get('products') as FormArray;

  onSubmit(menuForm: FormGroup) {
    ...
  }

...
}

如果我这样离开后端,我会出错:

Cannot read property 'controls' of undefined at MenuEditComponent.getProducts

所以我的 'products' FormArray 没有初始化。 如果我这样做:

  getProducts(form) {
    return form.controls.products?.controls;
  }

我不再有错误,但我无法访问产品 formControls。 如果我尝试:

  onLoadProducts(i: number) {
    const courseControl = this.menuForm.get('courses') as FormArray;
    const course = courseControl.at(i).get('courseName').value;
    const products: ProductModel[] = this.mapProducts.get(course);
    const productControl = this.menuForm.get('courses').get[i].get('products') as FormArray;
    //On the last line i get a 'Cannot read property 'get' of undefined'
...
}

我做错了什么?为什么我的 'products' FormArray 没有初始化?

非常感谢您的帮助!

courses 是 formGroup 的数组,在 formGroup products 中是另一个 formArray,因此您必须将单个课程 formGroup 传递给 getProducts 方法。

component.html

<div class="row" formArrayName="courses">
  <div *ngFor="let course of getCourses(menuForm); let i = index" [formGroupName]="i">
    <div class="...">
      <select class="form-control" formControlName="courseName">
        [value]="course"
        <option *ngFor="let course of courses">{{ course.name }}</option>
      </select>
      <button type="button" class="mt-2 btn btn-outline-dark" (click)="onLoadProducts(i)">
        Load Products
      </button>
      <div class="row" formArrayName="products">
        <div class="col-md-12" *ngFor="let product of getProducts(course); let j = index" [formGroupName]="j">
          <input class="form-check-input" type="checkbox" id="product{{ i }}-{{ j }}" formControlName="product" />
          <label class="form-check-label" for="product{{ i }}-{{ j }}">product</label>
        </div>
      </div>
    </div>
  </div>
</div>

然后使用FormGroup中的get方法获取子控件。

component.ts

getCourses(form) {
    return form.get('courses').controls;
}

getProducts(form) {
    return form.get('products').controls;
}

Example

我查看了您的 stackblitz 并对其进行了修改并使其正常运行。您不需要将 formControlName 与您的产品绑定,因为您不是从前端 UI 设置它们,而只是选择它们。我在 class ProductModel 中添加了一个名为 added 的字段,然后我刚刚为复选框实现了 change 事件以将其标记为真或假。

您还可以看到我是如何为产品设置价值的。当您点击 Load Products 按钮时,您将添加空的 FormGroup,但您还应该使用产品数据在其中设置 product 控件。您可以在代码中看到我在 LoadProducts 方法中是如何做的

之后您可以在产品循环中看到我是如何获取产品字段的。该循环实际上给了我们 FormGroup 我们必须从中获取产品字段值

这是正在做这项工作的 stackblitz。

https://stackblitz.com/edit/angular-ivy-ohwv1v

特别注意这部分

if (this.productsByCategory) {
  this.productsByCategory.forEach(
  (product) => {
    const productForm: FormGroup = this.initProduct();
    productForm.controls.product.setValue(product);
      control.push(productForm);
    });
}

创建 productFormGroup 后,您还应该在其中设置产品。喜欢 productForm.controls.product.setValue(product);

然后在前端你可以看到

<div *ngFor="let p of getProducts(course); let j = index" [formGroupName]="j">
  <input type="checkbox" id="product{{i}}-{{j}}" [value]="p.value.product.added" (change)="p.value.product.added = !p.value.product.added"/>
  <label  for="product{{i}}-{{j}}">{{p.value.product.name}}</label>
</div>

您应该将值绑定到产品的添加属性,并且名称也应该显示在产品对象的标签内。

希望这能帮助您继续前进。

And the second problem is that the labels of my checkboxes are not independent. If i add a new 'course' and that i select some products, the labels of all checkboxes in other courses are changed too.

以上问题已解决

I don't know how to be able to retrieve something like:

而不是 productName: true/false 你有整个产品对象可用,added 字段将标记它是否 checked/unchecked