当我们构建Angular应用程序时,形式的创建和构建是手动过程。小型更改的维护,例如添加新字段或从输入切换字段类型到日期字段应该很容易,但是为什么不创建表单以灵活并响应业务更改?
这些更改需要我们再次触摸表单,以更新硬编码的表单声明和字段。为什么不更改以使我们的形式动态而不是硬编码?
今天,我们将学习如何使用模型中的反应性形式创建形式,并将它们绑定到模板动态中的输入,无线电和复选框。
。我知道代码需要类型和接口,但我想制作可管理的文章。
设想
我们为营销团队工作,该团队希望一份表格要求用户的名称,姓氏和年龄。让我们构建它。
首先,声明FormGroup
字段registerForm
并创建方法buildForm()
以手动添加formGroup中的每个字段。
import { Component, OnInit, VERSION } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
registerForm: FormGroup;
ngOnInit() {
this.buildForm();
}
buildForm() {
this.registerForm = new FormGroup({
name: new FormControl(''),
lastName: new FormControl(''),
age: new FormControl(''),
});
}
}
使用[formGroup
]指令链接的输入添加HTML标记。
<h1>Register</h1>
<form [formGroup]="registerForm">
<label>Name:<input type="text" formControlName="name" /></label>
<label>LastName: <input type="text" formControlName="lastName" /></label>
<label> Age: <input type="number" formControlName="age" /></label>
</form>
最后,我们有静态形式!
但是明天,营销希望向新闻通讯请求地址或复选框。我们需要更新表单声明,添加formControl
和输入,并且您知道整个过程。
我们希望避免每次重复相同的任务。我们需要打开表单对动态和对业务变化做出反应,而无需再次触摸HTML或Typescript文件。
创建动态formcontrol和输入
首先,我们将有两个任务来构建我们的表单动态。
- 从业务对象创建表单组。
- 显示以形式渲染的字段列表。
首先,将registerModel
重命名为model
并声明字段数组。它将具有我们动态形式的输入模型的所有元素:
fields: [];
model = {
name: '',
lastName: '',
address: '',
age: '',
};
接下来,使用theformGroupFields
对象和
创建方法getFormControlFields()
使用for
迭代模型中的所有属性,以推入formGroupFields
。将每个字段添加到fields
阵列中。
代码看起来像这样:
getFormControlsFields() {
const formGroupFields = {};
for (const field of Object.keys(this.model)) {
formGroupFields[field] = new FormControl("");
this.fields.push(field);
}
return formGroupFields;
}
在buildForm()
方法中,添加带有getFormControlFields()
值的变量formGroupFields
,并将formGroupFields
分配给registerForm
。
buildForm() {
const formGroupFields = this.getFormControlsFields();
this.registerForm = new FormGroup(formGroupFields);
}
接下来,使用*ngFor
指令在fields
阵列上迭代。使用变量field
显示标签并设置具有相同字段值的输入[formControlName
]。
<form [formGroup]="registerForm">
<div *ngFor="let field of fields">
<label>{{field}}</label>
<input type="text" [formControlName]="field"/>
</form>
保存更改,我们从定义中动态生成相同的形式。
,但这只是开始。我们希望分解一些责任,以使我们能够使这种形式灵活地改变而不会痛苦。
分开表单过程和fieldType
app.component
执行了一些任务,创建模型,表单和渲染输入。让我们稍微清理一下。
创建具有输入属性的dynamic-form.component
,以使模型生成,并将registerForm
更新为dynamicFormGroup
。将功能buildForm
和getFormControlsFields
移动到动态。
import { Component, Input, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
@Component({
selector: "app-dynamic-form",
templateUrl: "./dynamic-form.component.html",
styleUrls: ["./dynamic-form.component.css"],
})
export class DynamicFormComponent implements OnInit {
dynamicFormGroup: FormGroup;
@Input() model: {};
fields = [];
ngOnInit() {
this.buildForm();
}
buildForm() {
const formGroupFields = this.getFormControlsFields();
this.dynamicFormGroup = new FormGroup(formGroupFields);
}
getFormControlsFields() {
const formGroupFields = {};
for (const field of Object.keys(this.model)) {
formGroupFields[field] = new FormControl("");
this.fields.push(field);
}
return formGroupFields;
}
}
记住将html中的formGroup
更新为dynamicFormGroup
接下来,创建新的组件dynamic-field
,负责渲染该领域。添加两个Input()
属性field
和formName
。
import {Component, Input} from "@angular/core";
@Component({
selector: "app-field-input",
templateUrl: "./dynamic-field.component.html",
styleUrls: ["./dynamic-field.component.css"],
})
export class DynamicFieldComponent {
@Input() field: {};
@Input() formName: string;
}
添加带有输入和标签的HTML
标记。
<form [formGroup]="formName">
<label>{{field}}</label>
<input type="text" [formControlName]="field"/>
</form>
打开app.component
以将模型传递到动态形式。它负责处理模型,而dynamic-field
呈现了输入。
import { Component } from "@angular/core";
@Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent {
model = {
name: "",
lastName: "",
address: "",
age: "",
};
}
HTML
通过定义传递属性模型。
<app-dynamic-form [model]="model"></app-dynamic-form>
完美,我们有一些单独的任务和责任。以下挑战显示了不同的控制类型。
按类型显示输入
动态形式呈现单一类型的输入。在现实世界中,我们需要更多类型的类型,例如date
,select
,input
,radio
和checkbox
。
有关控制类型的信息必须从模型到dynamic-field
。
使用以下属性type
,value
和label
更改model
。为了使它有点有趣,请更改type number
的年龄,并创建一个新的birthDay
of type date
。
model = {
firstname: {
type: "text",
value: "",
label: "FirstName",
},
lastname: {
type: "text",
value: "",
label: "LastName",
},
address: {
type: "text",
value: "",
label: "Address",
},
age: {
type: "number",
value: "",
label: "age",
},
birthDay: {
type: "date",
value: "",
label: "Birthday",
},
};
}
保存形式中显示的新字段birthDay
。
我们将对getFormsControlsFields
方法进行小更改以处理元数据。
请创建一个新变量,fieldProps
,以使用模型中的元数据存储该字段。使用值属性来分配formControl
,并在字段数组中使用属性fieldName
推动字段。
我们将在动态场组件中使用元数据属性
getFormControlsFields() {
const formGroupFields = {};
for (const field of Object.keys(this.model)) {
const fieldProps = this.model[field];
formGroupFields[field] = new FormControl(fieldProps.value);
this.fields.push({ ...fieldProps, fieldName: field });
}
return formGroupFields;
}
最后,转到dynamic.component.html
并使用这些属性field.label
,更改formControlName
以使用field.fieldName
,然后用field.type
绑定类型。
<form [formGroup]="formName">
<label>{{field.label}}</label>
<input [type]="field.type" [formControlName]="field.fieldName"/>
</form>
保存更改,并使用类型查看新控件。
添加选择,收音机和复选框
动态字段组件显示了输入,但是添加诸如select
,radio
或checkbox
之类的控件使其变得有些复杂。我想将每个控件分为特定的控件。
为每个控制dynamic-input
dynamic-radio
,dynamic-select
和dynamic-checkbox
创建组件。
ng g components/dynamic-field/dynamic-checkbox
ng g components/dynamic-field/dynamic-radio
ng g components/dynamic-field/dynamic-select
ng g components/dynamic-field/dynamic-input
每个组件都有两个共同点,带有元数据和FormGroup
的字段都可以使用主形式。
让我们从输入和复选框开始:
输入
用元数据将field
对象声明为输入属性。
export class DynamicInputComponent {
@Input() field: {};
@Input() formName: FormGroup;
}
在html标记中,使用标签和formControlName
与fieldName
使用元数据。
<form [formGroup]="formName">
<label>{{field.label}}</label>
<input [type]="field.type" [formControlName]="field.fieldName"/>
</form>
复选框
喜欢dynamic-input component
,添加两个字段,带有字段元数据和formGroup
。
export class DynamicCheckboxsComponent {
@Input() field: any;
@Input() formName: FormGroup;
}
在HTML标记中,添加一个复选框。
<form [formGroup]="formName">
<label>
{{ field.label }}
<input
type="checkbox"
[name]="field.fieldName"
[formControlName]="field.fieldName"
[value]="field.value"
/>
</label>
</form>
出于个人原因,我想将复选框与输入分开;复选框有时具有特定的样式。
选择
属性是相同的,但是元数据会变得不同。选择有一个选项列表,因此我们需要使用 ngfor 指令迭代列表。
HTML标记看起来像这样:
<form [formGroup]="formName">
<label>{{field.label}}:</label>
<select [formControlName]="field.fieldName">
<option *ngFor="let option of field.options" [value]="option.value">
{{option.label}}
</option>
</select>
</form>
收音机
收音机很接近,类似于options
列表的选择,但是在特定情况下,该名称必须相同以允许选择一个选项。我们添加了额外的label
来显示选项label
。
<form [formGroup]="formName">
<h3>{{field.label}}</h3>
<label *ngFor="let option of field.options">
<label ngFor="let option of field.options">
<input type="radio"
[name]="field.fieldName"
[formControlName]="field.fieldName"
[value]="option.value"
>
{{option.label}}
</label>
</label>
</form>
好的,所有组件都准备就绪,但是有两个缺失点:显示组件并更新元数据。
显示动态组件和更新模型
我们有每种控制类型的组件,但是dynamic-field.component
是它们之间的桥梁。
它按类型选择特定的组件。使用ngSwitch
指令,我们确定与组件类型的控制匹配。
最终代码看起来像这样:
<ng-container [ngSwitch]="field.type">
<app-dynamic-input *ngSwitchCase="'text'" [formName]="formName" [field]="field"></app-dynamic-input>
<app-dynamic-select *ngSwitchCase="'select'" [formName]="formName" [field]="field"></app-dynamic-select>
<app-dynamic-radio *ngSwitchCase="'radio'" [formName]="formName" [field]="field"></app-dynamic-radio>
<app-dynamic-checkboxs *ngSwitchCase="'checkbox'" [formName]="formName" [field]="field"></app-dynamic-checkboxs>
</ng-container>
了解有关switchCase
的更多信息接下来,我们为每种类型的元数据添加新字段:
typebussines:radio
SISSCRIPTYTYPE:select
新闻通讯:checkbox
类型radio
和select
必须具有带有{ label, value}
fit组件期望的选项对象。
typeBussines: {
label: "Bussines Type",
value: "premium",
type: "radio",
options: [
{
label: "Enterprise",
value: "1500",
},
{
label: "Home",
value: "6",
},
{
label: "Personal",
value: "1",
},
],
},
newsletterIn: {
label: "Suscribe to newsletter",
value: "email",
type: "checkbox"
},
suscriptionType: {
label: "Suscription Type",
value: "premium",
type: "select",
options: [
{
label: "Pick one",
value: "",
},
{
label: "Premium",
value: "premium",
},
{
label: "Basic",
value: "basic",
},
],
},
保存并重新加载。新组件可与结构一起使用,dynamic-field
选择了特定的组件。
验证
我们需要一个完整的表格,并具有验证。我想简要介绍这篇文章,但是验证在表格中至关重要。
我的示例是添加所需验证器的基本示例,但请随时添加更多。
首先,我们必须使用新的元数据rules
更改model
,并带有required
带有true
值。
firstname: {
type: "text",
value: "",
label: "FirstName",
rules: {
required: true,
}
},
验证器是表单控件的一部分。我们处理以新方法为addValidators
的formControl
的验证器的规则,并在validators
变量中存储的返回值以在formControl
中分配。
const validators = this.addValidator(fieldProps.rules);
formGroupFields[field] = new FormControl(fieldProps.value, validators);
如果规则对象为空,请返回一个空数组
在addValidator
中,使用Object.keys
并在rules
对象中的每个属性上进行迭代。使用switch case
进行数学值,然后返回Validator
。
在我们的情况下,所需的规则返回验证器。
private addValidator(rules) {
if (!rules) {
return [];
}
const validators = Object.keys(rules).map((rule) => {
switch (rule) {
case "required":
return Validators.required;
//add more cases for the future.
}
});
return validators;
}
好的,我们已经使用验证器配置了formControl
,但是如果控件无效,我们需要显示标签。创建一个新组件dynamic-error
,具有两个输入属性,formGroup
和fieldName
。
import { Component, Input } from "@angular/core";
import { FormGroup } from "@angular/forms";
@Component({
selector: "app-dynamic-error",
templateUrl: "./dynamic-error.component.html",
styleUrls: ["./dynamic-error.component.css"],
})
export class DynamicErrorComponent {
@Input() formName: FormGroup;
@Input() fieldName: string;
}
我们使用html中的表单参考来找到按名称的控件。如果是invalid
,dirty
或touched
,请显示消息。
<div *ngIf="formName.controls[fieldName].invalid && (formName.controls[fieldName].dirty || formName.controls[fieldName].touched)"
class="alert">
<div *ngIf="formName.controls[fieldName].errors.required">
* {{fieldName}}
</div>
</div>
最后,在dynamic-form
组件中添加dynamic-error
组件并通过fieldName
和formGroup
。
<form [formGroup]="dynamicFormGroup">
<div *ngFor="let field of fields">
<app-field-input [formName]="dynamicFormGroup" [field]="field"></app-field-input>
<app-dynamic-error [formName]="dynamicFormGroup" [fieldName]="field.fieldName"></app-dynamic-error>
</div>
</form>
是的!验证者使用我们的动态形式。
如果您到达此部分,则具有稳定的动态形式。我尝试使这篇文章简短,但是我听到了其他用户的反馈,例如 @Juan Berzosa Tejero和...。
重构时间
FormGroup的传播
@Juan Berzosa Tejero花点时间回顾了这篇文章,他向我询问了使用formName
的@Input()
的FormGroup
传播,然后开始发出噪音。幸运的是,我在角文档中找到了指令FormGroupDirective
。它可以帮助我们将现有的FormGroup
或FormRecord
绑定到DOM元素。
我决定使用和重构代码,我们将从dynamic-error.component
开始以简化,但是所有子组件的步骤都相似。
从formName
中卸下@Input()
装饰器,然后将FormGroupDirective
注入组件构造函数。
添加ngOnInit
生命周期以用FormGroupDirective.control
设置formName
以将FormGroup
绑定到它。
最终代码看起来像这样:
import { Component, Input, OnInit } from "@angular/core";
import { FormGroup, FormGroupDirective } from "@angular/forms";
@Component({
selector: "app-dynamic-error",
templateUrl: "./dynamic-error.component.html",
styleUrls: ["./dynamic-error.component.css"],
})
export class DynamicErrorComponent implements OnInit {
formName: FormGroup;
@Input() fieldName: string;
constructor(private formgroupDirective: FormGroupDirective) {}
ngOnInit() {
this.formName = this.formgroupDirective.control;
}
}
dynamic-form
不再需要通过formGroupName
。它只需要元数据。代码看起来像这样:
<form [formGroup]="dynamicFormGroup">
<div *ngFor="let field of fields">
<app-field-input [field]="field"></app-field-input>
<app-dynamic-error [fieldName]="field.fieldName"></app-dynamic-error>
</div>
</form>
如果您对所有子组件复制相同,则dynamic-field
不再需要设置formName
。
<ng-container [ngSwitch]="field.type">
<app-dynamic-input *ngSwitchCase="'text'" [field]="field"></app-dynamic-input>
<app-dynamic-input *ngSwitchCase="'number'" [field]="field"></app-dynamic-input>
<app-dynamic-select *ngSwitchCase="'select'" [field]="field"></app-dynamic-select>
<app-dynamic-radio *ngSwitchCase="'radio'" [field]="field"></app-dynamic-radio>
<app-dynamic-checkboxs *ngSwitchCase="'checkbox'" [field]="field"></app-dynamic-checkboxs>
</ng-container>
完成,我们做了重构!请随时阅读有关FormGroup Directive的更多信息。
删除Ngswitch
昨天, @Maxime Lyakhov,留下有关ngswich的消息。他对HTML的Ngswitch是正确的。很难维护。
我的第一个想法是使用ViewChild
和ViewContainerRef
动态加载特定组件,并使用setInput()
方法设置输入变量。
注意:我将项目更新为Angular 14,因为API to load dynamic components更容易。
首先,在ng-container
中添加模板变量,以使用ViewChild进行引用。
<ng-container #dynamicInputContainer>
</ng-container>
接下来,声明指向DynamicInput容器的ViewChild,它是我们动态组件的占位符。
@ViewChild('dynamicInputContainer', { read: ViewContainerRef}) dynamicInputContainer!: ViewContainerRef;
添加一个带有键和组件的所有受支持组件的新数组。
supportedDynamicComponents = [
{
name: 'text',
component: DynamicInputComponent
},
{
name: 'number',
component: DynamicInputComponent
},
{
name: 'select',
component: DynamicSelectComponent
},
{
name: 'radio',
component: DynamicRadioComponent
},
{
name: 'date',
component: DynamicInputComponent
},
{
name: 'checkbox',
component: DynamicCheckboxsComponent
}
]
注意:服务可以提供受支持的组件或外部变量的列表,但我尝试将文章简短。
创建getComponentByType
方法以在SuppertedDynamicComponents中找到组件,如果不存在返回DynamicInputComponent。
getComponentByType(type: string): any {
const componentDynamic = this.supportedDynamicComponents.find(c => c.name === type);
return componentDynamic.component || DynamicInputComponent;
}
接下来,一种新方法registerDynamicField()
。它负责从getComponentType()
创建实例并设置组件所需的输入字段。
我们执行三个步骤:
- 使用字段属性键入组件并存储在
componentInstance
变量中。 - 使用CreateComponent通过实例并获取动态组件。
- 使用
setInput()
方法将字段传递到输入field
。
private registerDynamicField() {
const componentInstance = this.getComponentByType(this.field.type)
const dynamicComponent = this.dynamicInputContainer.createComponent(componentInstance)
dynamicComponent.setInput('field', this.field);
this.cd.detectChanges();
}
由于输入属性字段更改,我们需要触发更改检测以保持组件同步。
_LEALN有关ChangeDetection_
的更多信息观看式的频场仅在AfterViewInit LifeCycle上可用,实现界面并调用方法registerDynamicField
。
ngAfterViewInit(): void {
this.registerDynamicField();
}
保存更改,一切都按预期继续工作,而Ngswitch消失了。
## recap
我们学会了如何从结构中添加动态字段,并生成诸如select
,checkbox
,radio
或inputs
的输入几次。该模型可以是后端的API响应。
如果要添加一个新字段,请将其添加到API响应中,您可以随意在动态场组件中添加更多类型。
我们可以通过使用每种组件类型的接口来改进代码,例如dropdown
,checkbox
或表单本身。另外,创建辅助功能以创建大多数下拉式的样板代码。
如果您愿意,请随时分享!