Angular 4.x 动态创建表单实例
本文将介绍如何动态创建表单组件,我们最终实现的效果如下:
在阅读本文之前,请确保你已经掌握 Angular 响应式表单和动态创建组件的相关知识,如果对相关知识还不了解,推荐先阅读一下 Angular 4.x Reactive Forms 和 Angular 4.x 动态创建组件 这两篇文章。对于已掌握的读者,我们直接进入主题。
创建动态表单
创建 DynamicFormModule
在当前目录先创建
dynamic-form目录,然后在该目录下创建
dynamic-form.module.ts文件,文件内容如下:
dynamic-form/dynamic-form.module.ts
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, ReactiveFormsModule ] }) export class DynamicFormModule{}
创建完
DynamicFormModule模块,接着我们需要在 AppModule 中导入该模块:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { DynamicFormModule} from './dynamic-form/dynamic-form.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule, DynamicFormModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { }
创建 DynamicForm 容器
进入
dynamic-form目录,在创建完
containers目录后,继续创建
dynamic-form目录,然后在该目录创建一个名为
dynamic-form.component.ts的文件,文件内容如下:
import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'dynamic-form', template: ` <form [formGroup]="form"> </form> ` }) export class DynamicFormComponentimplements OnInit { @Input() config: any[] = []; form: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit() { this.form = this.createGroup(); } createGroup(){ constgroup= this.fb.group({}); this.config.forEach(control => group.addControl(control.name, this.fb.control(''))); return group; } }
由于我们的表单是动态的,我们需要接受一个数组类型的配置对象才能知道需要动态创建的内容。因此,我们定义了一个
config输入属性,用于接收数组类型的配置对象。
此外我们利用了 Angular 响应式表单,提供的 API 动态的创建
FormGroup对象。对于配置对象中的每一项,我们要求该项至少包含两个属性,即 (type) 类型和 (name) 名称:
- type - 用于设置表单项的类型,如
input
、select
、button
等 - name - 用于设置表单控件的 name 属性
在
createGroup()方法中,我们循环遍历输入的
config属性,然后利用
FormGroup对象提供的
addControl()方法,动态地添加新建的表单控件。
接下来我们在 DynamicFormModule模块中声明并导出新建的
DynamicFormComponent组件:
import { DynamicFormComponent} from './containers/dynamic-form/dynamic-form.component'; @NgModule({ imports: [ CommonModule, ReactiveFormsModule ], declarations: [ DynamicFormComponent ], exports: [ DynamicFormComponent ] }) export class DynamicFormModule{}
现在我们已经创建了表单,让我们实际使用它。
使用动态表单
打开app.component.ts文件,在组件模板中引入我们创建的
dynamic-form组件,并设置相关的配置对象,具体示例如下:
app.component.ts
import { Component } from '@angular/core'; interface FormItemOption{ type: string; label: string; name: string; placeholder?: string; options?: string[] } @Component({ selector: 'exe-app', template: ` <div> <dynamic-form[config]="config"></dynamic-form> </div> ` }) export class AppComponent { config: FormItemOption[] = [ { type: 'input', label: 'Full name', name: 'name', placeholder: 'Enter your name' }, { type: 'select', label: 'Favourite food', name: 'food', options: ['Pizza', 'Hot Dogs', 'Knakworstje', 'Coffee'], placeholder: 'Select an option' }, { type: 'button', label: 'Submit', name: 'submit' } ]; }
上面代码中,我们在 AppComponent 组件类中设置了
config配置对象,该配置对象中设置了三种类型的表单类型。对于每个表单项的配置对象,我们定义了一个
FormItemOption数据接口,该接口中我们定义了三个必选属性:type、label 和 name 及两个可选属性:options 和 placeholder。下面让我们创建对应类型的组件。
自定义表单项组件
FormInputComponent
在
dynamic-form目录,我们新建一个
components目录,然后创建
form-input、
form-select和
form-button三个文件夹。创建完文件夹后,我们先来定义
form-input组件:
form-input.component.ts
import { Component, ViewContainerRef} from '@angular/core'; import {FormGroup} from '@angular/forms'; @Component({ selector: 'form-input', template: ` <div[formGroup]="group"> <label>{{config.label}}</label> <input type="text" [attr.placeholder]="config.placeholder" [formControlName]="config.name" /> </div> ` }) export class FormInputComponent { config: any; group: FormGroup; }
上面代码中,我们在 FormInputComponent 组件类中定义了
config和
group两个属性,但我们并没有使用
@Input装饰器来定义它们,因为我们不会以传统的方式来使用这个组件。接下来,我们来定义
select和
button组件。
FormSelectComponent
import { Component } from '@angular/core'; import {FormGroup} from '@angular/forms'; @Component({ selector: 'form-select', template: ` <div[formGroup]="group"> <label>{{config.label}}</label> <select[formControlName]="config.name"> <option value="">{{ config.placeholder }}</option> <option *ngFor="let option of config.options"> {{ option }} </option> </select> </div> ` }) export class FormSelectComponent { config: Object; group: FormGroup; }
FormSelectComponent 组件与 FormInputComponent 组件的主要区别是,我们需要循环配置中定义的options属性。这用于向用户显示所有的选项,我们还使用占位符属性,作为默认的选项。
FormButtonComponent
import { Component } from '@angular/core'; import {FormGroup} from '@angular/forms'; @Component({ selector: 'form-button', template: ` <div[formGroup]="group"> <buttontype="submit"> {{config.label}} </button> </div> ` }) export class FormButtonComponent{ config: Object; group: FormGroup; }
以上代码,我们只是定义了一个简单的按钮,它使用
config.label的值作为按钮文本。与所有组件一样,我们需要在前面创建的模块中声明这些自定义组件。打开
dynamic-form.module.ts文件并添加相应声明:
// ... import { FormButtonComponent} from './components/form-button/form-button.component'; import { FormInputComponent } from './components/form-input/form-input.component'; import { FormSelectComponent } from './components/form-select/form-select.component'; @NgModule({ // ... declarations: [ DynamicFormComponent, FormButtonComponent, FormInputComponent, FormSelectComponent ], exports: [ DynamicFormComponent ] }) export class DynamicFormModule{}
到目前为止,我们已经创建了三个组件。若想动态的创建这三个组件,我们将定义一个指令,该指令的功能跟
router-outlet指令类似。接下来在
components目录内部,我们新建一个
dynamic-field目录,然后创建
dynamic-field.directive.ts文件。该文件的内容如下:
import { Directive, Input } from '@angular/core'; import {FormGroup} from '@angular/forms'; @Directive({ selector: '[dynamicField]' }) export class DynamicFieldDirective{ @Input() config: Object; @Input() group: FormGroup; }
我们将指令的
selector属性设置为
[dynamicField],因为我们将其应用为属性而不是元素。
这样做的好处是,我们的指令可以应用在 Angular 内置的
<ng-container>指令上。
<ng-container>是一个逻辑容器,可用于对节点进行分组,但不作为 DOM 树中的节点,它将被渲染为 HTML中的
comment元素。因此配合
<ng-container>指令,我们只会在 DOM 中看到我们自定义的组件,而不会看到
<dynamic-field>元素 (因为 DynamicFieldDirective指令的selector被设置为 [dynamicField] )。
另外在指令中,我们使用
@Input装饰器定义了两个输入属性,用于动态设置
config和
group对象。接下来我们开始动态渲染组件。
动态渲染组件,我们需要用到
ComponentFactoryResolver和
ViewContainerRef两个对象。
ComponentFactoryResolver对象用于创建对应类型的组件工厂 (ComponentFactory),而
ViewContainerRef对象用于表示一个视图容器,可添加一个或多个视图,通过它我们可以方便地创建和管理内嵌视图或组件视图。
让我们在 DynamicFieldDirective指令构造函数中,注入相关对象,具体代码如下:
import { ComponentFactoryResolver, Directive, Input, OnInit, ViewContainerRef} from '@angular/core'; import {FormGroup} from '@angular/forms'; @Directive({ selector: '[dynamicField]' }) export class DynamicFieldDirectiveimplements OnInit { @Input() config; @Input() group: FormGroup; constructor( private resolver: ComponentFactoryResolver, private container: ViewContainerRef ) {} ngOnInit() { } }
上面代码中,我们还添加了
ngOnInit生命周期钩子。由于我们允许使用
input或
select类型来声明组件的类型,因此我们需要创建一个对象来将字符串映射到相关的组件类,具体如下:
// ... import { FormButtonComponent} from '../form-button/form-button.component'; import { FormInputComponent } from '../form-input/form-input.component'; import { FormSelectComponent } from '../form-select/form-select.component'; constcomponents= { button: FormButtonComponent, input: FormInputComponent, select: FormSelectComponent }; @Directive(...) export class DynamicFieldDirectiveimplements OnInit { // ... }
这将允许我们通过
components['button']获取对应的
FormButtonComponent组件类,然后我们可以把它传递给
ComponentFactoryResolver对象以获取对应的 ComponentFactory (组件工厂):
// ... constcomponents= { button: FormButtonComponent, input: FormInputComponent, select: FormSelectComponent }; @Directive(...) export class DynamicFieldDirectiveimplements OnInit { // ... ngOnInit() { const component = components[this.config.type]; const factory = this.resolver.resolveComponentFactory<any>(component); } // ... }
现在我们引用了配置中定义的给定类型的组件,并将其传递给 ComponentFactoryRsolver 对象提供的
resolveComponentFactory()方法。您可能已经注意到我们在 resolveComponentFactory 旁边使用了
<any>,这是因为我们要创建不同类型的组件。此外我们也可以定义一个接口,然后每个组件都去实现,如果这样的话
any就可以替换成我们已定义的接口。
现在我们已经有了组件工厂,我们可以简单地告诉我们的 ViewContainerRef为我们创建这个组件:
@Directive(...) export class DynamicFieldDirectiveimplements OnInit { // ... component: any; ngOnInit() { const component = components[this.config.type]; const factory = this.resolver.resolveComponentFactory<any>(component); this.component = this.container.createComponent(factory); } // ... }
我们现在已经可以将
config和
group传递到我们动态创建的组件中。我们可以通过
this.component.instance访问到组件类的实例:
@Directive(...) export class DynamicFieldDirectiveimplements OnInit { // ... component; ngOnInit() { const component = components[this.config.type]; const factory = this.resolver.resolveComponentFactory<any>(component); this.component = this.container.createComponent(factory); this.component.instance.config= this.config; this.component.instance.group= this.group; } // ... }
接下来,让我们在
DynamicFormModule中声明已创建的
DynamicFieldDirective指令:
// ... import { DynamicFieldDirective} from './components/dynamic-field/dynamic-field.directive'; @NgModule({ // ... declarations: [ DynamicFieldDirective, DynamicFormComponent, FormButtonComponent, FormInputComponent, FormSelectComponent ], exports: [ DynamicFormComponent ] }) export class DynamicFormModule{}
如果我们直接在浏览器中运行以上程序,控制台会抛出异常。当我们想要通过
ComponentFactoryResolver对象动态创建组件的话,我们需要在
@NgModule配置对象的一个属性 - entryComponents 中,声明需动态加载的组件。
@NgModule({ // ... entryComponents: [ FormButtonComponent, FormInputComponent, FormSelectComponent ] }) export class DynamicFormModule{}
基本工作都已经完成,现在我们需要做的就是更新
DynamicFormComponent组件,应用我们之前已经
DynamicFieldDirective实现动态组件的创建:
@Component({ selector: 'dynamic-form', template: ` <form class="dynamic-form" [formGroup]="form"> <ng-container *ngFor="let field of config;" dynamicField [config]="field" [group]="form"> </ng-container> </form> ` }) export class DynamicFormComponentimplements OnInit { // ... }
正如我们前面提到的,我们使用
<ng-container>作为容器来重复我们的动态字段。当我们的组件被渲染时,这是不可见的,这意味着我们只会在 DOM 中看到我们的动态创建的组件。
此外我们使用
*ngFor结构指令,根据config(数组配置项) 动态创建组件,并设置
dynamicField指令的两个输入属性:config和 group。最后我们需要做的是实现表单提交功能。
表单提交
我们需要做的是为我们的
<form>组件添加一个
(ngSubmit)事件的处理程序,并在我们的动态表单组件中新增一个
@Output输出属性,以便我们可以通知使用它的组件。
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'dynamic-form', template: ` <form [formGroup]="form" (ngSubmit)="submitted.emit(form.value)"> <ng-container *ngFor="let field of config;" dynamicField [config]="field" [group]="form"> </ng-container> </form> ` }) export class DynamicFormComponentimplements OnInit { @Input() config: any[] = []; @Output() submitted: EventEmitter<any> = new EventEmitter<any>(); // ... }
最后我们同步更新一下
app.component.ts文件:
import { Component } from '@angular/core'; @Component({ selector: 'exe-app', template: ` <div class="app"> <dynamic-form [config]="config" (submitted)="formSubmitted($event)"> </dynamic-form> </div> ` }) export class AppComponent { // ... formSubmitted(value: any) { console.log(value); } }
Toddmotto 大神线上完整代码请访问- toddmott/angular-dynamic-forms。
我有话说
在自定义表单控件组件中
[formGroup]="group"是必须的么?
form-input.component.ts
<div[formGroup]="group"> <label>{{config.label}}</label> <input type="text" [attr.placeholder]="config.placeholder" [formControlName]="config.name" /> </div>
如果去掉 <div> 元素上的[formGroup]="group" 属性,重新编译后浏览器控制台将会抛出以下异常:
Error:formControlNamemust be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it an existingFormGroupinstance (you can create one in your class). Example: <div [formGroup]="myGroup"> <inputformControlName="firstName"> </div> In your class: this.myGroup = new FormGroup({ firstName: new FormControl() });
在
formControlName指令中,初始化控件的时候,会验证父级指令的类型:
private _checkParentType(): void { if (!(this._parent instanceof FormGroupName) && this._parent instanceof AbstractFormGroupDirective) { ReactiveErrors.ngModelGroupException(); } else if ( !(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective) && !(this._parent instanceof FormArrayName)) { ReactiveErrors.controlParentException(); } }
那为什么要验证,是因为要把新增的控件添加到对应
formDirective对象中:
private _setUpControl() { this._checkParentType(); this._control = this.formDirective.addControl(this); if (this.control.disabled && this.valueAccessor !.setDisabledState) { this.valueAccessor !.setDisabledState !(true); } this._added = true; }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
您可能感兴趣的文章:
- 使用反射动态创建类的实例
- *函数参数传递类的类型,在函数内部动态创建不同类型的实例
- 利用反射来动态创建实例和调用方法
- .Net 中的反射(动态创建类型实例) - Part.4动态创建对象
- 在C# 编程中通过类名动态创建实例
- 在Delphi中调用VC++创建的动态链接库的实例
- [转载]使用反射技术动态创建类对象(实例代码)
- 应用Java程序动态创建ODBC数据源的实例
- .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)
- 在VC2005中,只要知道类的名字,就可以动态创建类的实例
- 用.net动态创建类的实例
- 用.net动态创建类的实例 (转)
- .Net中的反射(动态创建类型实例) - Part.4
- C#动态创建类的实例
- 利用反射来动态创建实例和调用方法
- 虽然动态创建DataGrid的代码在网上有很多,但是作为表单提交的时候却不多见?
- .Net 中的反射(动态创建类型实例) - Part.4
- 利用反射来动态创建实例和调用方法(上)
- .Net中的反射(动态创建类型实例) - Part.4
- C#动态创建类的实例