【问题标题】:How to get a nested FormArray control in Angular 9 ReactiveForm如何在 Angular 9 Reactive Form 中获取嵌套的 FormArray 控件
【发布时间】:2020-09-27 03:37:47
【问题描述】:

编辑

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

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

  "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
        }
      ]

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

我希望我的帖子不要太乱,我不经常在 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

所以我的“产品”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'
...
}

我做错了什么?为什么我的“产品”FormArray 没有初始化?

非常感谢您的帮助!

【问题讨论】:

  • 您编写的代码在某些方面没有意义。为什么要将formControlName='courseName' 绑定到select 列表?这意味着当您更改选项时,课程名称将在课程子表单中更新。其次,这条线let course of courses 是什么意思?你添加的课程不是用top form吗?还是这里的课程来源不同?你能把你所有的代码放在 stackblitz.com 上吗?我认为这段代码存在一些逻辑问题。
  • 感谢您的回答,您可能是对的,我对 Angular 还不太满意,我才刚刚开始使用它。和反应形式对我来说有点模糊。我将尝试创建一个 stackblitz,我以前从未使用过它。为了回答您的问题,我正在从 api 中检索课程列表,这些类别将填充选择输入,当我们选择其中一门课程时,它将从同一个 api 中获取该课程名称中的产品列表。我将尝试直接在 stackblitz 上创建它。感谢您的宝贵时间!
  • 这里是stackblitz:stackblitz.com/edit/angular-ivy-bzh3dh我会更新我的帖子来描述我现在遇到的问题!

标签: angular typescript angular-reactive-forms formarray


【解决方案1】:

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

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

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

这是正在完成这项工作的堆栈闪电战。

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>

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

希望这将帮助您进一步进行。

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

上述问题已解决

我不知道如何检索类似的内容:

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

【讨论】:

  • 你花了这么多时间给我一个完整的解决方案真是太棒了!明天我会深入研究它,因为它今天真的杀了我!但我看到它完美无缺!我真的不知道该怎么感谢你!简直太完美了!非常感谢!
【解决方案2】:

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

【讨论】:

  • 感谢您的回答,它有效,但我遇到了其他我不知道如何处理的问题。产品已显示,但我无法正确检索值,以及如何分别在每门课程上动态显示标签。我可能会在 Stack Overflow 上发布一个新问题!谢谢!
  • 你想在哪里访问 onLoadProducts 方法中的产品值?
  • 我不确定我的做法是否正确。我从 api 中按类别(这是我的“课程”)检索产品列表,我希望能够选择其中的一些。使用您的解决方案,我设法显示产品列表,但我的一个问题是,如果我选择项目,我会得到输出{...courses": [{"courseName": "Entrée","products": [{"product": true},{"product": false}...]},所以我无法确定选择了哪些产品,哪些没有。我想在 products 数组中将我的产品名称作为键。我不知道我应该编辑我的帖子还是创建一个新帖子!
  • 您的意思是在 onLoadProducts 中您想获得所选课程的价值?
  • 我设法做到了,我现在的问题是使用和检索产品复选框中的正确值。我在这里stackblitz.com/edit/angular-ivy-bzh3dh 做了一个stackblitz,我更新了我的帖子,试图让它更清楚!
猜你喜欢
  • 2018-07-15
  • 1970-01-01
  • 2020-10-02
  • 2021-05-02
  • 2021-03-07
  • 2021-06-04
  • 1970-01-01
  • 2023-01-05
  • 2021-05-30
相关资源
最近更新 更多