本文将介绍如何动态建立表单组件,咱们最终实现的效果以下:html
在阅读本文以前,请确保你已经掌握 Angular 响应式表单和动态建立组件的相关知识,若是对相关知识还不了解,推荐先阅读一下 Angular 4.x Reactive Forms 和 Angular 4.x 动态建立组件 这两篇文章。对于已掌握的读者,咱们直接进入主题。git
在当前目录先建立 dynamic-form
目录,而后在该目录下建立 dynamic-form.module.ts
文件,文件内容以下:github
dynamic-form/dynamic-form.module.tstypescript
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, ReactiveFormsModule ] }) export class DynamicFormModule {}
建立完 DynamicFormModule
模块,接着咱们须要在 AppModule 中导入该模块:bootstrap
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 { }
进入 dynamic-form
目录,在建立完 containers
目录后,继续建立 dynamic-form
目录,而后在该目录建立一个名为 dynamic-form.component.ts
的文件,文件内容以下:segmentfault
import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormBuilder } from '@angular/forms'; @Component({ selector: 'dynamic-form', template: ` <form [formGroup]="form"> </form> ` }) export class DynamicFormComponent implements OnInit { @Input() config: any[] = []; form: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit() { this.form = this.createGroup(); } createGroup() { const group = 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
等app
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。下面让咱们建立对应类型的组件。
在 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
组件。
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属性。这用于向用户显示全部的选项,咱们还使用占位符属性,做为默认的选项。
import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; @Component({ selector: 'form-button', template: ` <div [formGroup]="group"> <button type="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 DynamicFieldDirective implements 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'; const components = { button: FormButtonComponent, input: FormInputComponent, select: FormSelectComponent }; @Directive(...) export class DynamicFieldDirective implements OnInit { // ... }
这将容许咱们经过 components['button']
获取对应的 FormButtonComponent 组件类,而后咱们能够把它传递给 ComponentFactoryResolver
对象以获取对应的 ComponentFactory (组件工厂):
// ... const components = { button: FormButtonComponent, input: FormInputComponent, select: FormSelectComponent }; @Directive(...) export class DynamicFieldDirective implements OnInit { // ... ngOnInit() { const component = components[this.config.type]; const factory = this.resolver.resolveComponentFactory<any>(component); } // ... }
如今咱们引用了配置中定义的给定类型的组件,并将其传递给 ComponentFactoryRsolver 对象提供的resolveComponentFactory()
方法。您可能已经注意到咱们在 resolveComponentFactory 旁边使用了 <any>
,这是由于咱们要建立不一样类型的组件。此外咱们也能够定义一个接口,而后每一个组件都去实现,若是这样的话 any
就能够替换成咱们已定义的接口。
如今咱们已经有了组件工厂,咱们能够简单地告诉咱们的 ViewContainerRef 为咱们建立这个组件:
@Directive(...) export class DynamicFieldDirective implements 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 DynamicFieldDirective implements 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 DynamicFormComponent implements 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 DynamicFormComponent implements 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 大神线上示例 - angular-dynamic-forms,查看完整代码请访问 - 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: formControlName must be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it an existing FormGroup instance (you can create one in your class). Example: <div [formGroup]="myGroup"> <input formControlName="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; }