溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Angular 4.x 動(dòng)態(tài)創(chuàng)建表單實(shí)例

發(fā)布時(shí)間:2020-08-29 15:08:40 來(lái)源:腳本之家 閱讀:125 作者:semlinker 欄目:web開發(fā)

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

Angular 4.x 動(dòng)態(tài)創(chuàng)建表單實(shí)例

在閱讀本文之前,請(qǐng)確保你已經(jīng)掌握 Angular 響應(yīng)式表單和動(dòng)態(tài)創(chuàng)建組件的相關(guān)知識(shí),如果對(duì)相關(guān)知識(shí)還不了解,推薦先閱讀一下 Angular 4.x Reactive Forms 和 Angular 4.x 動(dòng)態(tài)創(chuàng)建組件 這兩篇文章。對(duì)于已掌握的讀者,我們直接進(jìn)入主題。

創(chuàng)建動(dòng)態(tài)表單

創(chuàng)建 DynamicFormModule

在當(dāng)前目錄先創(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 容器

進(jìn)入 dynamic-form 目錄,在創(chuàng)建完 containers 目錄后,繼續(xù)創(chuàng)建 dynamic-form 目錄,然后在該目錄創(chuàng)建一個(gè)名為 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;
 }
}

由于我們的表單是動(dòng)態(tài)的,我們需要接受一個(gè)數(shù)組類型的配置對(duì)象才能知道需要?jiǎng)討B(tài)創(chuàng)建的內(nèi)容。因此,我們定義了一個(gè) config 輸入屬性,用于接收數(shù)組類型的配置對(duì)象。

此外我們利用了 Angular 響應(yīng)式表單,提供的 API 動(dòng)態(tài)的創(chuàng)建 FormGroup 對(duì)象。對(duì)于配置對(duì)象中的每一項(xiàng),我們要求該項(xiàng)至少包含兩個(gè)屬性,即 (type) 類型和 (name) 名稱:

  1. type - 用于設(shè)置表單項(xiàng)的類型,如 input、selectbutton
  2. name - 用于設(shè)置表單控件的 name 屬性

createGroup() 方法中,我們循環(huán)遍歷輸入的 config 屬性,然后利用 FormGroup 對(duì)象提供的 addControl() 方法,動(dòng)態(tài)地添加新建的表單控件。

接下來(lái)我們?cè)?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)建了表單,讓我們實(shí)際使用它。

使用動(dòng)態(tài)表單

打開 app.component.ts 文件,在組件模板中引入我們創(chuàng)建的 dynamic-form 組件,并設(shè)置相關(guān)的配置對(duì)象,具體示例如下:

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'
 }
 ];
}

上面代碼中,我們?cè)?AppComponent 組件類中設(shè)置了 config 配置對(duì)象,該配置對(duì)象中設(shè)置了三種類型的表單類型。對(duì)于每個(gè)表單項(xiàng)的配置對(duì)象,我們定義了一個(gè) FormItemOption 數(shù)據(jù)接口,該接口中我們定義了三個(gè)必選屬性:type、label 和 name 及兩個(gè)可選屬性:options 和 placeholder。下面讓我們創(chuàng)建對(duì)應(yīng)類型的組件。

自定義表單項(xiàng)組件

FormInputComponent

dynamic-form 目錄,我們新建一個(gè) components 目錄,然后創(chuàng)建 form-input、form-select form-button 三個(gè)文件夾。創(chuàng)建完文件夾后,我們先來(lái)定義 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;
}

上面代碼中,我們?cè)?FormInputComponent 組件類中定義了 config group 兩個(gè)屬性,但我們并沒(méi)有使用 @Input 裝飾器來(lái)定義它們,因?yàn)槲覀儾粫?huì)以傳統(tǒng)的方式來(lái)使用這個(gè)組件。接下來(lái),我們來(lái)定義 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屬性。這用于向用戶顯示所有的選項(xiàng),我們還使用占位符屬性,作為默認(rèn)的選項(xiàng)。

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;
}

以上代碼,我們只是定義了一個(gè)簡(jiǎn)單的按鈕,它使用 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)建了三個(gè)組件。若想動(dòng)態(tài)的創(chuàng)建這三個(gè)組件,我們將定義一個(gè)指令,該指令的功能跟 router-outlet 指令類似。接下來(lái)在 components 目錄內(nèi)部,我們新建一個(gè) 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àn)槲覀儗⑵鋺?yīng)用為屬性而不是元素。

這樣做的好處是,我們的指令可以應(yīng)用在 Angular 內(nèi)置的 <ng-container> 指令上。 <ng-container> 是一個(gè)邏輯容器,可用于對(duì)節(jié)點(diǎn)進(jìn)行分組,但不作為 DOM 樹中的節(jié)點(diǎn),它將被渲染為 HTML中的 comment 元素。因此配合 <ng-container> 指令,我們只會(huì)在 DOM 中看到我們自定義的組件,而不會(huì)看到 <dynamic-field> 元素 (因?yàn)?DynamicFieldDirective 指令的 selector 被設(shè)置為 [dynamicField] )。

另外在指令中,我們使用 @Input 裝飾器定義了兩個(gè)輸入屬性,用于動(dòng)態(tài)設(shè)置 config group 對(duì)象。接下來(lái)我們開始動(dòng)態(tài)渲染組件。

動(dòng)態(tài)渲染組件,我們需要用到 ComponentFactoryResolver ViewContainerRef 兩個(gè)對(duì)象。ComponentFactoryResolver 對(duì)象用于創(chuàng)建對(duì)應(yīng)類型的組件工廠 (ComponentFactory),而 ViewContainerRef 對(duì)象用于表示一個(gè)視圖容器,可添加一個(gè)或多個(gè)視圖,通過(guò)它我們可以方便地創(chuàng)建和管理內(nèi)嵌視圖或組件視圖。

讓我們?cè)?DynamicFieldDirective 指令構(gòu)造函數(shù)中,注入相關(guān)對(duì)象,具體代碼如下:

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 生命周期鉤子。由于我們?cè)试S使用 input select 類型來(lái)聲明組件的類型,因此我們需要?jiǎng)?chuàng)建一個(gè)對(duì)象來(lái)將字符串映射到相關(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 {
 // ...
}

這將允許我們通過(guò) components['button'] 獲取對(duì)應(yīng)的 FormButtonComponent 組件類,然后我們可以把它傳遞給 ComponentFactoryResolver 對(duì)象以獲取對(duì)應(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 對(duì)象提供的resolveComponentFactory() 方法。您可能已經(jīng)注意到我們?cè)?resolveComponentFactory 旁邊使用了 <any>,這是因?yàn)槲覀円獎(jiǎng)?chuàng)建不同類型的組件。此外我們也可以定義一個(gè)接口,然后每個(gè)組件都去實(shí)現(xiàn),如果這樣的話 any 就可以替換成我們已定義的接口。

現(xiàn)在我們已經(jīng)有了組件工廠,我們可以簡(jiǎn)單地告訴我們的 ViewContainerRef 為我們創(chuàng)建這個(gè)組件:

@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 傳遞到我們動(dòng)態(tài)創(chuàng)建的組件中。我們可以通過(guò) this.component.instance 訪問(wèn)到組件類的實(shí)例:

@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;
 }
 // ...
}

接下來(lái),讓我們?cè)?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 {}

如果我們直接在瀏覽器中運(yùn)行以上程序,控制臺(tái)會(huì)拋出異常。當(dāng)我們想要通過(guò) ComponentFactoryResolver 對(duì)象動(dòng)態(tài)創(chuàng)建組件的話,我們需要在 @NgModule 配置對(duì)象的一個(gè)屬性 - entryComponents 中,聲明需動(dòng)態(tài)加載的組件。

@NgModule({
 // ...
 entryComponents: [
 FormButtonComponent,
 FormInputComponent,
 FormSelectComponent
 ]
})
export class DynamicFormModule {}

基本工作都已經(jīng)完成,現(xiàn)在我們需要做的就是更新 DynamicFormComponent 組件,應(yīng)用我們之前已經(jīng) DynamicFieldDirective 實(shí)現(xiàn)動(dòng)態(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>作為容器來(lái)重復(fù)我們的動(dòng)態(tài)字段。當(dāng)我們的組件被渲染時(shí),這是不可見的,這意味著我們只會(huì)在 DOM 中看到我們的動(dòng)態(tài)創(chuàng)建的組件。

此外我們使用 *ngFor 結(jié)構(gòu)指令,根據(jù) config (數(shù)組配置項(xiàng)) 動(dòng)態(tài)創(chuàng)建組件,并設(shè)置 dynamicField 指令的兩個(gè)輸入屬性:config 和 group。最后我們需要做的是實(shí)現(xiàn)表單提交功能。

表單提交

我們需要做的是為我們的 <form> 組件添加一個(gè) (ngSubmit) 事件的處理程序,并在我們的動(dòng)態(tài)表單組件中新增一個(gè) @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 大神線上完整代碼請(qǐng)?jiān)L問(wèn)- toddmott/angular-dynamic-forms。

我有話說(shuō)

在自定義表單控件組件中 [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" 屬性,重新編譯后瀏覽器控制臺(tái)將會(huì)拋出以下異常:

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 指令中,初始化控件的時(shí)候,會(huì)驗(yàn)證父級(jí)指令的類型:

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àn)證,是因?yàn)橐研略龅目丶砑拥綄?duì)應(yīng) formDirective 對(duì)象中:

private _setUpControl() {
 this._checkParentType();
 this._control = this.formDirective.addControl(this);
 if (this.control.disabled && this.valueAccessor !.setDisabledState) {
 this.valueAccessor !.setDisabledState !(true);
 }
 this._added = true;
}

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI