Angular 2 - 大规模申请表格的处理

时间:2016-11-25 14:04:36

标签: forms angular architecture

在我正在为之工作的公司,我们正在开发一个具有多种形式的大型应用程序,用户需要填写这些应用程序才能注册我们的程序。当所有问题都得到回答后,用户就会到达一个部分,该部分总结了所有答案,突出显示无效答案,并让用户有机会重新访问上述任何一个表格步骤并修改他们的答案。这个逻辑将在一系列顶级部分重复,每个部分都有多个步骤/页面和一个摘要页面。

为实现这一目标,我们为每个单独的表单步骤(它们是“个人详细信息”或“资格”等类别)创建了一个组件,以及它们各自的路由和摘要页面的组件。

为了尽可能保持DRY,我们开始创建一个“主”服务,其中包含所有不同表单步骤(值,有效性等)的信息。

import { Injectable } from '@angular/core';
import { Validators } from '@angular/forms';
import { ValidationService } from '../components/validation/index';

@Injectable()
export class FormControlsService {
  static getFormControls() {
    return [
      {
        name: 'personalDetailsForm$',
        groups: {
          name$: [
            {
              name: 'firstname$',
              validations: [
                Validators.required,
                Validators.minLength(2)
              ]
            },
            {
              name: 'lastname$',
              validations: [
                Validators.required,
                Validators.minLength(2)
              ]
            }
          ],
          gender$: [
            {
              name: 'gender$',
              validations: [
                Validators.required
              ]
            }
          ],
          address$: [
            {
              name: 'streetaddress$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'city$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'state$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'zip$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'country$',
              validations: [
                Validators.required
              ]
            }
          ],
          phone$: [
            {
              name: 'phone$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'countrycode$',
              validations: [
                Validators.required
              ]
            }
          ],
        }
      },
      {
        name: 'parentForm$',
        groups: {
          all: [
            {
              name: 'parentName$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'parentEmail$',
              validations: [
                ValidationService.emailValidator
              ]
            },
            {
              name: 'parentOccupation$'
            },
            {
              name: 'parentTelephone$'
            }
          ]
        }
      },
      {
        name: 'responsibilitiesForm$',
        groups: {
          all: [
            {
              name: 'hasDrivingLicense$',
              validations: [
                Validators.required,
              ]
            },
            {
              name: 'drivingMonth$',
              validations: [
                ValidationService.monthValidator
              ]
            },
            {
              name: 'drivingYear$',
              validations: [
                ValidationService.yearValidator
              ]
            },
            {
              name: 'driveTimesPerWeek$',
              validations: [
                Validators.required
              ]
            },
          ]
        }
      }
    ];
  }
}

所有组件都使用该服务,以便为每个组件设置HTML表单绑定,方法是访问相应的对象键并创建嵌套表单组,以及“摘要”页面,其表示层仅为1绑定(模型 - >视图)。

export class FormManagerService {
    mainForm: FormGroup;

    constructor(private fb: FormBuilder) {
    }

    setupFormControls() {
        let allForms = {};
        this.forms = FormControlsService.getFormControls();

        for (let form of this.forms) {

            let resultingForm = {};

            Object.keys(form['groups']).forEach(group => {

                let formGroup = {};
                for (let field of form['groups'][group]) {
                    formGroup[field.name] = ['', this.getFieldValidators(field)];
                }

                resultingForm[group] = this.fb.group(formGroup);
            });

            allForms[form.name] = this.fb.group(resultingForm);
        }

        this.mainForm = this.fb.group(allForms);
    }

    getFieldValidators(field): Validators[] {
        let result = [];

        for (let validation of field.validations) {
            result.push(validation);
        }

        return (result.length > 0) ? [Validators.compose(result)] : [];
    }
}

之后,我们开始在组件中使用以下语法,以便访问主表单服务中指定的表单控件:

personalDetailsForm$: AbstractControl;
streetaddress$: AbstractControl;

constructor(private fm: FormManagerService) {
    this.personalDetailsForm$ = this.fm.mainForm.controls['personalDetailsForm$'];
    this.streetaddress$ = this.personalDetailsForm$['controls']['address$']['controls']['streetaddress$'];
}

这似乎是我们没有经验的眼睛里的代码味道。考虑到我们最终将拥有的部分数量,我们强烈关注这样的应用程序将如何扩展。

我们一直在讨论不同的解决方案,但是我们无法想出一个利用Angular的表单引擎的解决方案,允许我们保持我们的验证层次完整且简单。

有没有更好的方法来实现我们想要做的事情?

5 个答案:

答案 0 :(得分:3)

您的方法和Ovangle's one看起来还不错,但是即使这个问题已解决,我也想分享我的解决方案,因为这是一种非常不同的方法,我认为您可能会喜欢或对其他人有用

  

针对应用程序范围的表单有什么解决方案,其中组件负责处理全局表单的不同子部分。

我们已经遇到了完全相同的问题,经过数月的巨大,嵌套,有时是多态形式的苦苦挣扎,我们提出了一个使我们满意的解决方案,该解决方案易于使用,并赋予我们“超能力” (例如TS和HTML中的类型安全),访问嵌套错误和其他错误。

我们已经决定将其提取到一个单独的库中并对其进行开源。
此处提供源代码:https://github.com/cloudnc/ngx-sub-form
然后可以像这样npm i ngx-sub-form

来安装npm软件包。

在后台,我们的库使用ControlValueAccessor,它允许我们在模板形式和反应形式上使用它(不过,通过使用反应形式,您将获得最大的收获)。

那是怎么回事?

一个例子值得一千个单词,所以让我们重做表单的一部分(最难的是嵌套数据的表单):personalDetailsForm$

要做的第一件事是确保所有内容都将是类型安全的。让我们为它创建接口:

export enum Gender {
  MALE = 'Male',
  FEMALE = 'Female',
  Other = 'Other',
}

export interface Name {
  firstname: string;
  lastname: string;
}

export interface Address {
  streetaddress: string;
  city: string;
  state: string;
  zip: string;
  country: string;
}

export interface Phone {
  phone: string;
  countrycode: string;
}

export interface PersonalDetails {
  name: Name;
  gender: Gender;
  address: Address;
  phone: Phone;
}

export interface MainForm {
  // this is one example out of what you posted
  personalDetails: PersonalDetails;

  // you'll probably want to add `parent` and `responsibilities` here too
  // which I'm not going to do because `personalDetails` covers it all :)
}

然后,我们可以创建扩展NgxSubFormComponent的组件。
我们称之为personal-details-form.component

@Component({
  selector: 'app-personal-details-form',
  templateUrl: './personal-details-form.component.html',
  styleUrls: ['./personal-details-form.component.css'],
  providers: subformComponentProviders(PersonalDetailsFormComponent)
})
export class PersonalDetailsFormComponent extends NgxSubFormComponent<PersonalDetails> {
  protected getFormControls(): Controls<PersonalDetails> {
    return {
      name: new FormControl(null, { validators: [Validators.required] }),
      gender: new FormControl(null, { validators: [Validators.required] }),
      address: new FormControl(null, { validators: [Validators.required] }),
      phone: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

这里没有什么要注意的:

  • NgxSubFormComponent<PersonalDetails>将为我们提供类型安全性
  • 我们必须实现getFormControls方法,该方法需要匹配抽象控件(此处为namegender,{{ 1}},address
  • 我们完全控制创建formControl的选项(验证器,异步验证器等)
  • phone是一个小的实用程序函数,用于创建使用providers: subformComponentProviders(PersonalDetailsFormComponent)(参见Angular doc)所需的提供程序,您只需要将当前组件作为参数传递即可。

现在,对于作为对象的ControlValueAccessornamegenderaddress的每个条目,我们都会为其创建一个子表单(因此在这种情况下,所有内容但phone)。

这是电话的示例:

gender

现在,让我们为其编写模板:

@Component({
  selector: 'app-phone-form',
  templateUrl: './phone-form.component.html',
  styleUrls: ['./phone-form.component.css'],
  providers: subformComponentProviders(PhoneFormComponent)
})
export class PhoneFormComponent extends NgxSubFormComponent<Phone> {
  protected getFormControls(): Controls<Phone> {
    return {
      phone: new FormControl(null, { validators: [Validators.required] }),
      countrycode: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

注意:

  • 我们定义了<div [formGroup]="formGroup"> <input type="text" placeholder="Phone" [formControlName]="formControlNames.phone"> <input type="text" placeholder="Country code" [formControlName]="formControlNames.countrycode"> </div> ,此处<div [formGroup]="formGroup">formGroup提供,您不必自己创建
  • NgxSubFormComponent,我们使用属性绑定来创建动态[formControlName]="formControlNames.phone",然后使用formControlNameformControlNames也提供了这种类型的安全机制,如果您的界面在某个时候发生了更改(我们都知道重构...),那么不仅TS会因为表单中的属性缺失而出错,而且HTML也会因格式错误而出错(当您使用AOT编译)!

下一步:我们构建NgxSubFormComponent模板,但首先只需将该行添加到TS:PersonalDetailsFormComponent中,以便我们可以从视图安全地访问枚举

public Gender: typeof Gender = Gender;

注意我们如何将职责委派给子组件? <div [formGroup]="formGroup"> <app-name-form [formControlName]="formControlNames.name"></app-name-form> <select [formControlName]="formControlNames.gender"> <option *ngFor="let gender of Gender | keyvalue" [value]="gender.value">{{ gender.value }}</option> </select> <app-address-form [formControlName]="formControlNames.address"></app-address-form> <app-phone-form [formControlName]="formControlNames.phone"></app-phone-form> </div> 就是这里的重点!

最后一步:构建了顶部表单组件

好消息,我们也可以使用<app-name-form [formControlName]="formControlNames.name"></app-name-form>来享受类型安全性!

NgxSubFormComponent

和模板:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent extends NgxSubFormComponent<MainForm> {
  protected getFormControls(): Controls<MainForm> {
    return {
      personalDetails: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

enter image description here

所有这些:  -输入安全表格  -可重复使用!是否需要为<form [formGroup]="formGroup"> <app-personal-details-form [formControlName]="formControlNames.personalDetails"></app-personal-details-form> </form> <!-- let see how the form values looks like! --> <h1>Values:</h1> <pre>{{ formGroupValues | json }}</pre> <!-- let see if there's any error (works with nested ones!) --> <h1>Errors:</h1> <pre>{{ formGroupErrors | json }}</pre> 重用地址1?当然,不用担心  -不错的实用程序,用于构建嵌套表单,访问表单控件名称,表单值,表单错误(+嵌套!)  -您是否注意到任何复杂的逻辑?没有可观察到的东西,没有注入服务...仅定义接口,扩展类,将对象与表单控件一起传递并创建视图。就这样

顺便说一下,这是我一直在谈论的所有内容的实时演示
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling

在这种情况下,也没有必要,但是对于形式稍微复杂一些,例如,当您需要处理parents之类的多态对象时,我们为{{1 }},但是如果您需要更多信息,可以阅读README

希望它可以帮助您扩展表格!

答案 1 :(得分:2)

我在其他地方评论@ngrx/store,虽然我仍然推荐它,但我相信我对你的问题有些误解。

无论如何,你的FormsControlService基本上是一个全局const。说真的,用{/ 1>替换export class FormControlService ...

export const formControlsDefinitions = {
   // ...
};

它有什么区别?您只需导入对象,而不是获取服务。由于我们现在将其视为一个类型化的const全局,我们可以定义我们使用的接口......

export interface ModelControl<T> {
    name: string;
    validators: ValidatorFn[];
}

export interface ModelGroup<T> {
   name: string;
   // Any subgroups of the group
   groups?: ModelGroup<any>[];
   // Any form controls of the group
   controls?: ModelControl<any>[];
}

并且由于我们已经这样做了,我们可以将单个表单组的定义移出单个整体模块,并定义我们定义模型的表单组。更干净。

// personal_details.ts

export interface PersonalDetails {
  ...
}

export const personalDetailsFormGroup: ModelGroup<PersonalDetails> = {
   name: 'personalDetails$';
   groups: [...]
}

但现在我们将所有这些单独的表单组定义分散在我们的模块中,并且无法全部收集它们:(我们需要一些方法来了解我们应用程序中的所有表单组。

但是我们不知道将来会有多少模块,我们可能想要延迟加载它们,因此它们的模型组可能不会在应用程序启动时注册。

对救援的控制倒置!让我们创建一个服务,只需一个注入依赖项 - 一个多提供程序,当我们在整个模块中分发它们时,可以注入所有分散的表单组。

export const MODEL_GROUP = new OpaqueToken('my_model_group');

/**
 * All the form controls for the application
 */
export class FormControlService {
    constructor(
        @Inject(MMODEL_GROUP) rootControls: ModelGroup<any>[]
    ) {}

    getControl(name: string): AbstractControl { /etc. }
}

然后在某处创建一个清单模块(将其注入“核心”应用程序模块),构建您的FormService

@NgModule({
   providers : [
     {provide: MODEL_GROUP, useValue: personalDetailsFormGroup, multi: true}
     // and all your other form groups
     // finally inject our service, which knows about all the form controls
     // our app will ever use.
     FormControlService
   ]
})
export class CoreFormControlsModule {}

我们现在有一个解决方案:

  • 更本地化,表单控件与模型一起声明
  • 更具可扩展性,只需添加表单控件然后将其添加到清单模块;和
  • 少整体,没有“上帝”配置类。

答案 2 :(得分:1)

我做了类似的申请。问题是您正在同时创建所有输入,这可能不可扩展。

就我而言,我做了一个管理FormGroup数组的FormManagerService。每个步骤都有一个FormGroup,通过将FormGroup配置发送到FormManagerService,在执行步骤组件的ngOnInit时初始化一次。这样的事情:

stepsForm: Array<FormGroup> = [];
getFormGroup(id:number, config: Object): FormGroup {
    let formGroup: FormGroup;
    if(this.stepsForm[id]){
        formGroup = this.stepsForm[id];
    } else {
        formGroup = this.createForm(config); // call function to create FormGroup
        this.stepsForm[id] = formGroup;
    }
    return formGroup;
}

您需要一个ID才能知道哪个FormGroup对应于该步骤。但在那之后,您将能够在每个步骤中拆分Forms配置(这样的小配置文件比大文件更易于维护)。它将最小化初始加载时间,因为FormGroups仅在需要时创建。

最后在提交之前,您只需要映射FormGroup数组并验证它们是否全部有效。只需确保已访问过所有步骤(否则将无法创建某个FormGroup)。

这可能不是最好的解决方案,但它非常适合我的项目,因为我强迫用户按照我的步骤操作。 给我你的反馈意见。 :)

答案 3 :(得分:0)

此答案带有以下警告:我是一个黑客,几乎什么都不知道。如果错误,请随时撕开。至少对我来说,我对 Ovangle 的回答不够了解,无法实现,并且我需要知道如何使用FormArray才能使用看起来很棒的 Maxime1992 库。 / p>

绕了一圈后,除了找到一种形式,一个组成部分之外,没有找到许多形式的例子,并找到了这个古老的问题,它询问了我想知道的90%(具有不同路线的子形式),我提出了以下内容我共享的模式,以防对其他人有用:

模式

  • 各个子表单创建一个服务,该服务提供FormGroup以及 Create Delete 方法。
  • 容器使用此服务并将其传递给子组件(表单和表格)
  • 一个表单,它更新表中的选定行。
  • 演示文稿以显示(读取)表单数据。可以单击行以发出行以供表单编辑

主表单导入子表单的个人服务。

粗糙的堆叠闪电战-https://stackblitz.com/edit/angular-uzmdmu-merge-formgroups

Schematic of form service

让我知道是否可以改善

答案 4 :(得分:-1)

是否真的有必要将表单控件保留在服务中?为什么不将服务作为数据的守护者,并在组件中使用表单控件?您可以使用CanDeactivate防护来阻止用户导航远离包含无效数据的组件。

https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html