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的表单引擎,允许我们保持我们的验证层次完整,也很简单.
有没有更好的方法来实现我们想要做的事情?
我在其他地方对进行了评论@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)
我们现在有一个解决方案是:
您的方法和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其预期相匹配的抽象控制顶层键的字典方法(在这里name,gender,address,phone)providers: subformComponentProviders(PersonalDetailsFormComponent)是一个小的实用程序函数,用于创建使用ControlValueAccessor(cf Angular doc)所需的提供程序,您只需要将当前组件作为参数传递现在,对于每个条目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] }),
};
}
}
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的安全表格
| 归档时间: |
|
| 查看次数: |
1925 次 |
| 最近记录: |