在我正在为之工作的公司,我们正在开发一个具有多种形式的大型应用程序,用户需要填写这些应用程序才能注册我们的程序。当所有问题都得到回答后,用户就会到达一个部分,该部分总结了所有答案,突出显示无效答案,并让用户有机会重新访问上述任何一个表格步骤并修改他们的答案。这个逻辑将在一系列顶级部分重复,每个部分都有多个步骤/页面和一个摘要页面。
为实现这一目标,我们为每个单独的表单步骤(它们是“个人详细信息”或“资格”等类别)创建了一个组件,以及它们各自的路由和摘要页面的组件。
为了尽可能保持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的表单引擎的解决方案,允许我们保持我们的验证层次完整且简单。
有没有更好的方法来实现我们想要做的事情?
答案 0 :(得分:3)
您的方法和Ovangle's one看起来还不错,但是即使这个问题已解决,我也想分享我的解决方案,因为这是一种非常不同的方法,我认为您可能会喜欢或对其他人有用
针对应用程序范围的表单有什么解决方案,其中组件负责处理全局表单的不同子部分。
我们已经遇到了完全相同的问题,经过数月的巨大,嵌套,有时是多态形式的苦苦挣扎,我们提出了一个使我们满意的解决方案,该解决方案易于使用,并赋予我们“超能力” (例如TS和HTML中的类型安全),访问嵌套错误和其他错误。
我们已经决定将其提取到一个单独的库中并对其进行开源。
此处提供源代码:https://github.com/cloudnc/ngx-sub-form
然后可以像这样npm i ngx-sub-form
在后台,我们的库使用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
方法,该方法需要匹配抽象控件(此处为name
,gender
,{{ 1}},address
)phone
是一个小的实用程序函数,用于创建使用providers: subformComponentProviders(PersonalDetailsFormComponent)
(参见Angular doc)所需的提供程序,您只需要将当前组件作为参数传递即可。现在,对于作为对象的ControlValueAccessor
,name
,gender
,address
的每个条目,我们都会为其创建一个子表单(因此在这种情况下,所有内容但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"
,然后使用formControlName
。 formControlNames
也提供了这种类型的安全机制,如果您的界面在某个时候发生了更改(我们都知道重构...),那么不仅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] }),
};
}
}
所有这些:
-输入安全表格
-可重复使用!是否需要为<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
让我知道是否可以改善
答案 4 :(得分:-1)
是否真的有必要将表单控件保留在服务中?为什么不将服务作为数据的守护者,并在组件中使用表单控件?您可以使用CanDeactivate
防护来阻止用户导航远离包含无效数据的组件。
https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html