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

Man*_*lis 16 architecture forms angular

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

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

为了尽可能保持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
              ]
            },
          ]
        }
      }
    ];
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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)] : [];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

personalDetailsForm$: AbstractControl;
streetaddress$: AbstractControl;

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

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

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

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

ova*_*gle 5

我在其他地方对进行了评论@ngrx/store,尽管我仍然推荐它,但我认为我对您的问题有些误解。

无论如何,您FormsControlService基本上是一个全局常量。严重的是,更换export class FormControlService ...

export const formControlsDefinitions = {
   // ...
};
Run Code Online (Sandbox Code Playgroud)

它有什么区别?无需获取服务,只需导入对象即可。并且由于我们现在将其视为类型化的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>[];
}
Run Code Online (Sandbox Code Playgroud)

既然这样做了,我们就可以将单个表单组的定义移出单个整体模块,并在定义模型的地方定义表单组。干净得多。

// personal_details.ts

export interface PersonalDetails {
  ...
}

export const personalDetailsFormGroup: ModelGroup<PersonalDetails> = {
   name: 'personalDetails$';
   groups: [...]
}
Run Code Online (Sandbox Code Playgroud)

但是现在我们所有这些单独的表单组定义分散在我们的模块中,而没有办法收集它们全部:(我们需要某种方式来了解应用程序中的所有表单组。

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

控制权急救!让我们提供一个具有单个注入依赖项的服务-一个多提供者,当我们在整个模块中分配它们时,可以将其与所有分散的表单组一起注入。

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. }
}
Run Code Online (Sandbox Code Playgroud)

然后在某处创建清单模块(注入到“核心”应用模块中),构建您的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 {}
Run Code Online (Sandbox Code Playgroud)

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

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


max*_*992 5

您的方法和Ovangle的方法似乎很好,但是即使解决了这个SO问题,我也想分享我的解决方案,因为这是一种非常不同的方法,我认为您可能喜欢或对其他人有用。

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

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

我们决定将其提取到一个单独的库中并开源。
源代码可在此处获取:https : //github.com/cloudnc/ngx-sub-form
可以像这样安装npm软件包npm i ngx-sub-form

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

那到底是什么呢?

在开始解释之前,如果您喜欢跟随适当的编辑者,我举了一个Stackblitz示例:https ://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling

好吧,我猜一个例子值一千个单词,所以让我们重做表单的一部分(最难的是嵌套数据的表单): 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 :)
}
Run Code Online (Sandbox Code Playgroud)

然后,我们可以创建一个可扩展的组件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] }),
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

这里没有什么要注意的:

  • NgxSubFormComponent<PersonalDetails> 将给我们类型安全
  • 我们必须实现getFormControls其预期相匹配的抽象控制顶层键的字典方法(在这里namegenderaddressphone
  • 我们完全控制创建formControl的选项(验证器,异步验证器等)
  • providers: subformComponentProviders(PersonalDetailsFormComponent)是一个小的实用程序函数,用于创建使用ControlValueAccessor(cf Angular doc)所需的提供程序,您只需要将当前组件作为参数传递

现在,对于每个条目namegenderaddressphone这是一个对象,我们为它创建一个子表(所以在这种情况下,一切不过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] }),
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

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

<div [formGroup]="formGroup">
  <input type="text" placeholder="Phone" [formControlName]="formControlNames.phone">
  <input type="text" placeholder="Country code" [formControlName]="formControlNames.countrycode">
</div>
Run Code Online (Sandbox Code Playgroud)

注意:

  • 我们定义<div [formGroup]="formGroup">formGroup这里是NgxSubFormComponent您提供的,不必自己创建
  • [formControlName]="formControlNames.phone"我们使用属性绑定进行动态处理formControlName,然后使用formControlNames。也提供了这种类型的安全机制NgxSubFormComponent,如果您的界面在某个时候发生了更改(我们都知道重构...),那么不仅您的TS会因为表单中的缺少属性而出错,而且还会因HTML(当您使用AOT进行编译时)而出错。 !

下一步:让我们构建PersonalDetailsFormComponent模板,但首先只需将该行添加到TS中: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>
Run Code Online (Sandbox Code Playgroud)

注意我们如何将职责委派给子组件?<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] }),
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

和模板:

<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>
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

所有这些的要点:-输入安全表格-可重复使用!需要重新使用一个地址parents吗?当然,不用担心-用于构建嵌套表单,访问表单控件名称,表单值,表单错误(+嵌套!)的实用程序-您是否注意到任何复杂的逻辑?没有可观察到的东西,没有注入服务...仅定义接口,扩展类,将对象与表单控件一起传递并创建视图。而已

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

另外,在这种情况下,这不是必需的,但是对于形式来说则稍微复杂一些,例如,当您需要处理多态对象时,例如type Animal = Cat | Dog我们有另一个类,NgxSubFormRemapComponent但是如果您需要更多信息,则可以阅读自述文件。 。

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

编辑:

如果您想走得更远,我刚刚在这里发布了一篇博客文章,以解释有关表单和ngx-sub-form的许多内容,网址为https://dev.to/maxime1992/building-scalable-robust-and-type-带3nf9的安全表格