将表单分为多个组件并进行验证

86

我想用Angular 2创建一个单一的大表单,但我希望像以下示例所示那样使用多个组件来创建此表单。

应用程序组件

<form novalidate #form1="ngForm" [formGroup]="myForm">
<div>
    <address></address>
</div>
<div>
    <input type="text" ngModel required/>
</div>

<input type="submit" [disabled]="!form1.form.valid" > </form>

地址组件

<div>
<input type="text" ngModel required/> </div>
当我使用上述代码时,在浏览器中可以看到我需要的内容,但是当我删除地址组件中的文本时,提交按钮没有被禁用。但是当我删除应用程序组件中输入框中的文本时,该按钮正确地被禁用了。

你的地址组件是什么样子的?它是一个ControlValueAccessor吗? - Ján Halaša
只是一个简单的组件,类体中没有任何内容。 - Lasitha Yapa
尝试将以下内容添加到模板底部: {{ form1.value | json }} 看看其中是否包含两个输入元素或仅一个。 我知道表单无法拆分为使用路由器加载的组件。它也可能无法在嵌套组件中“找到”项目。 - DeborahK
@DeborahK 是的,您说得对,地址组件中的元素在 JSON 中没有显示出来。有什么办法可以实现这个吗? - Lasitha Yapa
1
虽然AJT82的答案很好并且非常有帮助,但它是一种与所要求的不同方法的解决方案。我在这里提供了一个如何在多个组件上实现表单的解决方案(在此处:https://dev59.com/YlgQ5IYBdhLWcg3wIAWb#53118308)(向下滚动)。 - dannybucks
6个回答

131

我会使用反应式表单,它非常好用。关于你的评论:

有没有其他更简单的例子?也许是没有循环的相同例子

我可以给你一个例子。你只需要将一个FormGroup嵌套并传递给子组件即可。

假设你的表单如下所示,并且你想将address formgroup传递给子组件:

ngOnInit() {
  this.myForm = this.fb.group({
    name: [''],
    address: this.fb.group({ // create nested formgroup to pass to child
      street: [''],
      zip: ['']
    })
  })
}

然后在您的父FormGroup中,只需传递嵌套的FormGroup:

<address [address]="myForm.get('address')"></address>

在您的子组件中,使用@Input来处理嵌套的formgroup:

@Input() address: FormGroup;

在您的模板中使用[formGroup]

<div [formGroup]="address">
  <input formControlName="street">
  <input formControlName="zip">
</div>

如果您不想创建实际的嵌套formgroup,那么您无需这样做,您只需要将父表单传递给子表单即可。如果您的表单看起来像:

this.myForm = this.fb.group({
  name: [''],
  street: [''],
  zip: ['']
})

您可以传递任何您想要的控件。使用上面的例子,我们只想显示streetzip,子组件保持不变,但是模板中的子标签将如下所示:

<child>
  <street />
  <zip />
</child>
<address [address]="myForm"></address>

这里是第一个选项的演示,这里是第二个演示

有关嵌套模型驱动表单的更多信息,请在这里查看。


这种方法需要你在父组件中复制与子组件相同的 FormGroup 定义。在你上面的例子中,父组件声明了 address,其中包含 street n zip 字段,但是地址组件也有相同的 FormGroup 定义(即街道和邮政编码),除非我误解了你的解决方案。如果嵌套层数很多,表单很复杂,那么将子代码复制到父级中会变得混乱。避免这种情况的方法是将父级表单传递给子组件,我不是专家:(还在学习中。 - Raf
1
@RenilBabu 你可以将整个表单传递给子组件。 - AT82
不起作用...抛出错误以创建一个 formGroup,然后在子组件中使用它。 - Renil Babu
@RenilBabu,这应该可以工作,你能为此创建一个演示吗?我很乐意看一下 :) - AT82
嗨,@Alex,我刚刚在 https://stackoverflow.com/questions/48843445/cannot-read-property-of-undefined-passing-formgroup-to-child-component-angular 发布了一个问题,关于我在遵循上面的示例时遇到的困难。如果您有时间,能否看一下? - David
显示剩余2条评论

20

在基于模板的表单中也有一种方法可以实现这一点。ngModel 会为每个组件自动创建一个独立的表单,但是您可以通过将以下内容添加到组件中来注入父组件的表单:

@Component({
viewProviders: [{ provide: ControlContainer, useExisting: NgForm}]
}) export class ChildComponent

但是,您必须确保每个输入都具有唯一的名称。因此,如果您使用*ngFor来调用子组件,则必须将索引(或任何其他唯一标识符)放入名称中,例如:

[name]="'address_' + i"

如果您想将表单结构化为FormGroup,则可以使用ngModelGroup

viewProviders: [{ provide: ControlContainer, useExisting: NgModelGroup }]

使用[ngModelGroup]="yourNameHere"替代ngForm,并将其添加到一些包含标签的子组件HTML中。


这个可以工作,但我无法弄清楚如何在使用viewProvider时设置测试。您能展示一个测试例子吗? - Jared Christensen
我还没有使用viewProviders进行测试,所以恐怕无法帮助您。但是,如果您解决了这个问题,分享一下就太好了! - dannybucks

10
根据我的经验,使用基于模板的表单很难制作这种形式的表单字段组合。嵌入在地址组件中的字段不会在表单(NgForm.controls对象)中注册,因此在验证表单时不会考虑它们。
你可以创建一个ControlValueAccessor组件(接受ngModel属性),其中包含所有验证,但是难以显示验证错误并传播更改(地址被视为具有复杂值的单个表单字段)。
你可能可以将表单引用传递到Address组件中,并在其中注册内部控件,但我没有尝试过,似乎是一种奇怪的方法(我没有在其他地方看到过)。
你可以切换到响应式表单(而不是基于模板的表单),将表单组对象(表示地址)传递到Address组件中,并在表单定义中保留验证。你可以在这里看到一个例子 https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2

谢谢Jan。在发布这个问题之前,我看了一下那个链接。但是由于它与*ngFor循环结合在一起,我无法清楚地理解发生了什么。是否有其他简单的示例?也许是没有循环的相同示例。 - Lasitha Yapa
1
我找到的所有示例都使用数组和*ngFor,因为这可能是重用表单部件的最常见用例。但我认为这不会影响解决方案 - 只需不索引要进入Address组件的表单组并跳过formArray部分即可。 - Ján Halaša

8

使用最新版本的Angular 2+,您可以将表单拆分成多个组件,而无需将formGroup实例作为输入传递给子组件。

您可以通过将此提供程序导入到子组件中来实现:

viewProviders: [{provide: ControlContainer,useExisting: FormGroupDirective}]

然后您可以通过注入formGroupDirective来访问主表单:

constructor(private readonly formGroupDirective: FormGroupDirective)

1
这是真正的解决方案 - 让我省了很多时间来弄清如何传递 FormGroup。 - n4cer500
这个模板的样子是怎么样的? - andyrue

7

我想分享一种在我的案例中有效的方法。我创建了以下指令:

import { Directive } from '@angular/core';
import { ControlContainer, NgForm } from '@angular/forms';

@Directive({
  selector: '[appUseParentForm]',
  providers: [
    {
      provide: ControlContainer,
      useFactory: function (form: NgForm) {
        return form;
      },
      deps: [NgForm]
    }
  ]
})
export class UseParentFormDirective {
}

现在,如果您在子组件上使用此指令,例如:
<address app-use-parent-form></address>

将来自AddressComponent的控件添加到form1中。因此,表单的有效性也将取决于子组件内控件的状态。

仅在Angular 6中进行了检查


1

最受欢迎的评论是模板化的:我见过采用这种方法的项目,管理起来非常混乱。

我要感谢用户@elia的回答。

为了让它更方便一些,我们可以把提供者放在常量中:

import { Provider } from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';

export const FormPart: Provider = {
  provide: ControlContainer,
  useExisting: FormGroupDirective,
};

然后在所需的组件中导入它,例如:

@Component({
  ...
  viewProviders: [FormPart],
})

同时,创建一个指令,使用提供的构造函数代码,如果需要,提供父表单,这样我们可以将表单业务逻辑分成更小的块。

我试图探索Angular中装饰器函数如何通过这种viewProvider扩展@Component,但这是一个更复杂的话题,似乎不可能,所以我放弃了。很高兴听到其他人的经验。我认为这是Angular支持并使与复杂表单的工作更轻松的一个很好的特性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接