在 Angular 7 中处理大型响应式表单

Posted

技术标签:

【中文标题】在 Angular 7 中处理大型响应式表单【英文标题】:Handling big reactive forms in Angular 7 【发布时间】:2019-12-22 07:33:42 【问题描述】:

我正在尝试寻找一种更好的方法来处理复杂的角度形式。 表格真的很大,我需要找到一种方法来降低复杂性。

这里是表单结构的一个例子:


    "fieldA" : ...,
    "fieldB" : ...,
    "fieldC" : ...,
    "profile": 
        "username": ...,
        "email": ...,
        "firstName": ...,
        "lastName": ...,
        ...
    ,
    "settings": 
        "enableEmailNotification": ...,
        "notificationsEmail": ..., // required when enableEmailNotification
        ...
    ,
    ...

在某些情况下验证器会即时更改,例如当enableEmailNotification=true 时,组件会将Required 验证器添加到notificationsEmail

这里是研究过的选项:

选项 #0 - 经典

sample on github

这种方法使用一种形式和一种组件。

优点:

代码很多,但很简单

缺点:

所有逻辑都在一个地方。对于我来说,这个组件变得太大并且难以阅读或维护 UI 也变得足够大了

选项 #1 - 将 FormGroup 传递给子组件

sample on github

这种方法将formGroup 作为@Input() 属性发送到内部组件。

优点:

缩小部分视图

缺点:

表单创建和验证规则仍在父组件中 仅缩小视图大小 在根组件中创建了验证逻辑,但在子组件中显示错误

选项 #2 - 创建自定义 ControlValueAccessor

sample on github

基于this article,我们可以创建自定义的ControlValueAccessor,它将返回一个表单的一部分的对象。

优点:

以多种形式拆分表单。表单可以拆分为更小的独立部分。

缺点:

为表单值保留 JS 对象。看起来不太好

【问题讨论】:

【参考方案1】:

我对大型复杂表单的策略是拥有一个包装组件和子组件。每个子组件都有自己的表单服务,包装器有一个主表单服务,其中注入了子表单服务,考虑一下

@Component(
  selector: 'form-wrapper',
  template: `
    <form [formGroup]="form" (submit)="save()">
      <sub-form-a></sub-form-a>
      <sub-form-b></sub-form-b>
      <input type="submit" value="Submit Form">
    </form>
  `,
  providers: [MasterFormService, FormAService, FormBService]
)
export class FormWrapper 
  constructor(private formService: MasterFormService)  
  save() 
    // whatever save actions here
  


@Component( // form b compoent is basically the same
  selector: 'sub-form-a',
  template: `
    ... whatever template belongs to form a ..
  `
)
export class FormAComponent 
  form: FormGroup
  constructor(private formService: FormAService) 
    this.form = this.formService.form;
    // form a specific actions
  


@Injectable()
export class MasterFormService 
  form: FormGroup;
  constructor(private fb: FormBuilder, formAService: FormAService, formBService: FormBService) 
    this.form = this.fb.group(
      groupA: this.formAService.form,
      groupB: this.formBService.form,
    );
  


@Injectable() // formB service is basically the same
export class FormAService 
  form: FormGroup;
  constructor(private fb: FormBuilder) 
    this.form = this.fb.group(
      .. whatever fields belong to form a ..
    );
  

此方法创建高度可重用的子表单,并让您模块化/隔离表单逻辑和模板。我经常发现子表单通常属于多个地方,因此它使我的代码非常干燥。特别是在您的示例中,您可以轻松地在应用程序的其他地方重用设置表单和配置文件表单组件。一两次我什至再次嵌套了这个结构以获得极其复杂的形式。

缺点是结构可能看起来很复杂,但你很快就会习惯。

【讨论】:

谢谢。看起来很有趣。我会试一试的。 你将如何通知 sub-form-b​​ 关于 sub-form-a 的变化,例如根据 sub-form-a 中的值添加或删除所需的验证器【参考方案2】:

就个人而言,对于大型复杂表单,我希望将表单逻辑保留在一个组件中,这样很容易链接可观察对象,但使用几个服务作为助手。用于初始化表单和处理操作(返回新表单值、验证、授权等)

逻辑/跟踪在FormComponent 表单的初始化在FormInitService 动作在一个pr多个中处理FormActionService(s)

表单组件

export class FormComponent implements OnInit 
  constructor(
    private formActionService: FormActionService,
    private formInitService: FormInitService
  )  

  ngOnInit() 
    this.form = this.FormInitService.getForm();
    this._trackFieldA();
    this._trackProfile();
  

  // Track a single field
  private _trackFieldA() 
    this.form.controls.fieldA.valueChanges.pipe(
      distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
    ).subscribe(fieldA => 
      console.log('Field A Changed');
      this.formActionService.doSomething();
    );
  

  // Track a group
  // Use ['controls'] for nested controls to skip typechecking errors
  private _trackProfile() 
    combineLatest(
      this.form.controls.profile['controls'].username.valueChanges,
      this.form.controls.profile['controls'].email.valueChanges,
    ).pipe(
      distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
    ).subscribe(profile => 
      console.log('Some profile field changed');
      this.formActionService.doSomething();
    );
  

FormInitService

export class FormInitService 
  constructor(
    private formBuilder: FormBuilder
  )  

  public getForm(): FormGroup 
    return this.formBuilder.group(
      fieldA: 'Some init value',
      fieldB: 'Some init value',
      profile: this.formBuilder.group(
        username: 'Some init value',
        email: 'Some init value',
        ...
      ),
      ...
    );
  

FormActionService

export class FormActionService 
  public doSomething(): any | void 
    console.log('Something')
  

您在 FormComponent 和模板中仍然有相当多的代码,但它真的很容易阅读和维护。拆分成多个组件通常会变得非常混乱,尤其是在团队工作时,或者需要进行一些(巨大的)重构时。

【讨论】:

以上是关于在 Angular 7 中处理大型响应式表单的主要内容,如果未能解决你的问题,请参考以下文章

响应式表单-Angular高级编程

如何使用响应式表单将验证器添加到表单控件以从角度材料进行匹配输入

在 Angular 4 响应式表单中提交时显示验证消息

在 Angular 2 中,响应式表单不使用 ngModel 和 formArrayName 更新值

Angular 11 自定义 ISBN 验证器响应式表单

为啥我应该在构造函数而不是 ngOnInit 中创建我的 Angular2 响应式表单?