Angular 4.x 動態(tài)創(chuàng)建表單實例
本文將介紹如何動態(tài)創(chuàng)建表單組件,我們最終實現(xiàn)的效果如下:

在閱讀本文之前,請確保你已經(jīng)掌握 Angular 響應(yīng)式表單和動態(tài)創(chuàng)建組件的相關(guān)知識,如果對相關(guān)知識還不了解,推薦先閱讀一下 Angular 4.x Reactive Forms 和 Angular 4.x 動態(tài)創(chuàng)建組件 這兩篇文章。對于已掌握的讀者,我們直接進入主題。
創(chuàng)建動態(tài)表單
創(chuàng)建 DynamicFormModule
在當前目錄先創(chuàng)建 dynamic-form 目錄,然后在該目錄下創(chuàng)建 dynamic-form.module.ts 文件,文件內(nèi)容如下:
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 {}
創(chuàng)建完 DynamicFormModule 模塊,接著我們需要在 AppModule 中導(dǎo)入該模塊:
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 { }
創(chuàng)建 DynamicForm 容器
進入 dynamic-form 目錄,在創(chuàng)建完 containers 目錄后,繼續(xù)創(chuàng)建 dynamic-form 目錄,然后在該目錄創(chuàng)建一個名為 dynamic-form.component.ts 的文件,文件內(nèi)容如下:
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;
}
}
由于我們的表單是動態(tài)的,我們需要接受一個數(shù)組類型的配置對象才能知道需要動態(tài)創(chuàng)建的內(nèi)容。因此,我們定義了一個 config 輸入屬性,用于接收數(shù)組類型的配置對象。
此外我們利用了 Angular 響應(yīng)式表單,提供的 API 動態(tài)的創(chuàng)建 FormGroup 對象。對于配置對象中的每一項,我們要求該項至少包含兩個屬性,即 (type) 類型和 (name) 名稱:
- type - 用于設(shè)置表單項的類型,如
input、select、button等 - name - 用于設(shè)置表單控件的 name 屬性
在 createGroup() 方法中,我們循環(huán)遍歷輸入的 config 屬性,然后利用 FormGroup 對象提供的 addControl() 方法,動態(tài)地添加新建的表單控件。
接下來我們在 DynamicFormModule 模塊中聲明并導(dǎo)出新建的 DynamicFormComponent 組件:
import { DynamicFormComponent } from './containers/dynamic-form/dynamic-form.component';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule
],
declarations: [
DynamicFormComponent
],
exports: [
DynamicFormComponent
]
})
export class DynamicFormModule {}
現(xiàn)在我們已經(jīng)創(chuàng)建了表單,讓我們實際使用它。
使用動態(tài)表單
打開 app.component.ts 文件,在組件模板中引入我們創(chuàng)建的 dynamic-form 組件,并設(shè)置相關(guān)的配置對象,具體示例如下:
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 組件類中設(shè)置了 config 配置對象,該配置對象中設(shè)置了三種類型的表單類型。對于每個表單項的配置對象,我們定義了一個 FormItemOption 數(shù)據(jù)接口,該接口中我們定義了三個必選屬性:type、label 和 name 及兩個可選屬性:options 和 placeholder。下面讓我們創(chuàng)建對應(yīng)類型的組件。
自定義表單項組件
FormInputComponent
在 dynamic-form 目錄,我們新建一個 components 目錄,然后創(chuàng)建 form-input、form-select 和 form-button 三個文件夾。創(chuàng)建完文件夾后,我們先來定義 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 裝飾器來定義它們,因為我們不會以傳統(tǒng)的方式來使用這個組件。接下來,我們來定義 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 組件的主要區(qū)別是,我們需要循環(huán)配置中定義的options屬性。這用于向用戶顯示所有的選項,我們還使用占位符屬性,作為默認的選項。
FormButtonComponent
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 的值作為按鈕文本。與所有組件一樣,我們需要在前面創(chuàng)建的模塊中聲明這些自定義組件。打開 dynamic-form.module.ts 文件并添加相應(yīng)聲明:
// ...
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 {}
到目前為止,我們已經(jīng)創(chuàng)建了三個組件。若想動態(tài)的創(chuàng)建這三個組件,我們將定義一個指令,該指令的功能跟 router-outlet 指令類似。接下來在 components 目錄內(nèi)部,我們新建一個 dynamic-field 目錄,然后創(chuàng)建 dynamic-field.directive.ts 文件。該文件的內(nèi)容如下:
import { Directive, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Directive({
selector: '[dynamicField]'
})
export class DynamicFieldDirective {
@Input()
config: Object;
@Input()
group: FormGroup;
}
我們將指令的 selector 屬性設(shè)置為 [dynamicField],因為我們將其應(yīng)用為屬性而不是元素。
這樣做的好處是,我們的指令可以應(yīng)用在 Angular 內(nèi)置的 <ng-container> 指令上。 <ng-container> 是一個邏輯容器,可用于對節(jié)點進行分組,但不作為 DOM 樹中的節(jié)點,它將被渲染為 HTML中的 comment 元素。因此配合 <ng-container> 指令,我們只會在 DOM 中看到我們自定義的組件,而不會看到 <dynamic-field> 元素 (因為 DynamicFieldDirective 指令的 selector 被設(shè)置為 [dynamicField] )。
另外在指令中,我們使用 @Input 裝飾器定義了兩個輸入屬性,用于動態(tài)設(shè)置 config 和 group 對象。接下來我們開始動態(tài)渲染組件。
動態(tài)渲染組件,我們需要用到 ComponentFactoryResolver 和 ViewContainerRef 兩個對象。ComponentFactoryResolver 對象用于創(chuàng)建對應(yīng)類型的組件工廠 (ComponentFactory),而 ViewContainerRef 對象用于表示一個視圖容器,可添加一個或多個視圖,通過它我們可以方便地創(chuàng)建和管理內(nèi)嵌視圖或組件視圖。
讓我們在 DynamicFieldDirective 指令構(gòu)造函數(shù)中,注入相關(guān)對象,具體代碼如下:
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 類型來聲明組件的類型,因此我們需要創(chuàng)建一個對象來將字符串映射到相關(guān)的組件類,具體如下:
// ...
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'] 獲取對應(yīng)的 FormButtonComponent 組件類,然后我們可以把它傳遞給 ComponentFactoryResolver 對象以獲取對應(yīng)的 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);
}
// ...
}
現(xiàn)在我們引用了配置中定義的給定類型的組件,并將其傳遞給 ComponentFactoryRsolver 對象提供的resolveComponentFactory() 方法。您可能已經(jīng)注意到我們在 resolveComponentFactory 旁邊使用了 <any>,這是因為我們要創(chuàng)建不同類型的組件。此外我們也可以定義一個接口,然后每個組件都去實現(xiàn),如果這樣的話 any 就可以替換成我們已定義的接口。
現(xiàn)在我們已經(jīng)有了組件工廠,我們可以簡單地告訴我們的 ViewContainerRef 為我們創(chuàng)建這個組件:
@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);
}
// ...
}
我們現(xiàn)在已經(jīng)可以將 config 和 group 傳遞到我們動態(tài)創(chuàng)建的組件中。我們可以通過 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 中聲明已創(chuàng)建的 DynamicFieldDirective 指令:
// ...
import { DynamicFieldDirective } from './components/dynamic-field/dynamic-field.directive';
@NgModule({
// ...
declarations: [
DynamicFieldDirective,
DynamicFormComponent,
FormButtonComponent,
FormInputComponent,
FormSelectComponent
],
exports: [
DynamicFormComponent
]
})
export class DynamicFormModule {}
如果我們直接在瀏覽器中運行以上程序,控制臺會拋出異常。當我們想要通過 ComponentFactoryResolver 對象動態(tài)創(chuàng)建組件的話,我們需要在 @NgModule 配置對象的一個屬性 - entryComponents 中,聲明需動態(tài)加載的組件。
@NgModule({
// ...
entryComponents: [
FormButtonComponent,
FormInputComponent,
FormSelectComponent
]
})
export class DynamicFormModule {}
基本工作都已經(jīng)完成,現(xiàn)在我們需要做的就是更新 DynamicFormComponent 組件,應(yīng)用我們之前已經(jīng) DynamicFieldDirective 實現(xiàn)動態(tài)組件的創(chuàng)建:
@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>作為容器來重復(fù)我們的動態(tài)字段。當我們的組件被渲染時,這是不可見的,這意味著我們只會在 DOM 中看到我們的動態(tài)創(chuàng)建的組件。
此外我們使用 *ngFor 結(jié)構(gòu)指令,根據(jù) config (數(shù)組配置項) 動態(tài)創(chuàng)建組件,并設(shè)置 dynamicField 指令的兩個輸入屬性:config 和 group。最后我們需要做的是實現(xiàn)表單提交功能。
表單提交
我們需要做的是為我們的 <form> 組件添加一個 (ngSubmit) 事件的處理程序,并在我們的動態(tài)表單組件中新增一個 @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 大神線上完整代碼請訪問- 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();
}
}
那為什么要驗證,是因為要把新增的控件添加到對應(yīng) 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;
}
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
AngularJS實現(xiàn)動態(tài)切換樣式的方法分析
這篇文章主要介紹了AngularJS實現(xiàn)動態(tài)切換樣式的方法,結(jié)合實例形式分析了AngularJS事件響應(yīng)與樣式動態(tài)控制相關(guān)操作技巧,需要的朋友可以參考下2018-06-06
詳解angular中通過$location獲取路徑(參數(shù))的寫法
本篇文章主要介紹了詳解angular中通過$location獲取路徑(參數(shù))的寫法 ,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03
深入解析AngularJS框架中$scope的作用與生命周期
這篇文章主要介紹了AngularJS中$scope的作用與生命周期,包括在DOM中添加controller對象的相關(guān)用法,需要的朋友可以參考下2016-03-03
angular中實現(xiàn)控制器之間傳遞參數(shù)的方式
本篇文章主要介紹了angular中實現(xiàn)控制器之間傳遞參數(shù)的方式,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04

