Angular 4.x 动态创建表单实例


Posted in Javascript onApril 25, 2017

本文将介绍如何动态创建表单组件,我们最终实现的效果如下:

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 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) 名称:

  1. type - 用于设置表单项的类型,如 inputselectbutton
  2. 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-inputform-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">
 <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 大神线上完整代码请访问- 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;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
高效的表格行背景隔行变色及选定高亮的JS代码
Dec 04 Javascript
Jquery多选下拉列表插件jquery multiselect功能介绍及使用
May 24 Javascript
深入探寻javascript定时器
Jan 02 Javascript
Javascript控制div属性动态变化实例分析
Oct 08 Javascript
jQuery mobile 移动web(4)
Dec 20 Javascript
JS运动相关知识点小结(附弹性运动示例)
Jan 08 Javascript
使用Angular.js实现简单的购物车功能
Nov 21 Javascript
基本DOM节点操作
Jan 17 Javascript
jQuery插件jquery.kxbdmarquee.js实现无缝滚动效果
Feb 15 Javascript
vuex与组件联合使用的方法
May 10 Javascript
自定义Vue组件打包、发布到npm及使用教程
May 22 Javascript
JavaScript canvas实现跟随鼠标移动小球
Feb 09 Javascript
AngularJS动态菜单操作指令
Apr 25 #Javascript
Angular.js 4.x中表单Template-Driven Forms详解
Apr 25 #Javascript
详解JS中的attribute属性
Apr 25 #Javascript
node.js中debug模块的简单介绍与使用
Apr 25 #Javascript
Node.js利用debug模块打印出调试日志的方法
Apr 25 #Javascript
JS实现禁止高频率连续点击的方法【基于ES6语法】
Apr 25 #Javascript
json的结构与遍历方法实例分析
Apr 25 #Javascript
You might like
php calender(日历)二个版本代码示例(解决2038问题)
2013/12/24 PHP
PHP异常Parse error: syntax error, unexpected T_VAR错误解决方法
2014/05/06 PHP
PHP支付系统设计与典型案例分享
2016/08/02 PHP
php 猴子摘桃的算法
2017/06/20 PHP
Laravel中获取路由参数Route Parameters的五种方法示例
2017/09/29 PHP
利用php获得flv视频长度的实例代码
2017/10/26 PHP
关于JavaScript的一些看法
2009/05/27 Javascript
Jquery中显示隐藏的实现代码分析
2011/07/26 Javascript
ie8 不支持new Date(2012-11-10)问题的解决方法
2013/07/31 Javascript
jquery如何获取复选框的值
2013/12/12 Javascript
javascript多行字符串的简单实现方式
2015/05/04 Javascript
详解JavaScript中双等号引起的隐性类型转换
2016/05/30 Javascript
Spring MVC中Ajax实现二级联动的简单实例
2016/07/06 Javascript
jQuery元素属性操作实例(设置、获取及删除元素属性)
2016/09/08 Javascript
jQuery实现手机版页面翻页效果的简单实例
2016/10/05 Javascript
vuex中的 mapState,mapGetters,mapActions,mapMutations 的使用
2018/04/13 Javascript
Vue插件打包与发布的方法示例
2018/08/20 Javascript
通过Kettle自定义jar包供javascript使用
2020/01/29 Javascript
Python 装饰器原理、定义与用法详解
2019/12/07 Python
Python日志:自定义输出字段 json格式输出方式
2020/04/27 Python
python3+openCV 获取图片中文本区域的最小外接矩形实例
2020/06/02 Python
python利用递归方法实现求集合的幂集
2020/09/07 Python
python中复数的共轭复数知识点总结
2020/12/06 Python
一款html5 canvas实现的图片玻璃碎片特效
2014/09/11 HTML / CSS
韩国演唱会订票网站:StubHub韩国
2019/01/17 全球购物
Chain Reaction Cycles俄罗斯:世界上最大的在线自行车商店
2019/08/27 全球购物
读书心得体会
2013/12/28 职场文书
保险公司早会主持词
2014/03/22 职场文书
导游个人求职信范文
2014/03/23 职场文书
超越自我演讲稿
2014/05/21 职场文书
殡葬服务心得体会
2014/09/11 职场文书
党的群众路线对照检查材料思想汇报
2014/09/25 职场文书
领导班子专题民主生活会情况想汇报
2014/09/30 职场文书
财务工作失职检讨书
2014/11/21 职场文书
交通事故死亡赔偿协议书
2014/12/03 职场文书
「SHOW BY ROCK!!」“雫シークレットマインド”组合单曲MV公开
2022/03/21 日漫