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 相关文章推荐
javascript客户端遍历控件与获取父容器对象示例代码
Jan 06 Javascript
原生javascript实现DIV拖拽并计算重复面积
Jan 02 Javascript
简介alert()与console.log()的不同
Aug 26 Javascript
JS实现的简洁二级导航菜单雏形效果
Oct 13 Javascript
js实现的简单图片浮动效果完整实例
May 10 Javascript
BootStrap Table对前台页面表格的支持实例讲解
Dec 22 Javascript
javascript实现循环广告条效果
Dec 12 Javascript
vue 监听某个div垂直滚动条下拉到底部的方法
Sep 15 Javascript
微信小程序实现省市区三级地址选择
Jun 21 Javascript
JavaScript设计模式之享元模式实例详解
Jan 17 Javascript
jQuery实现的图片点击放大缩小功能案例
Jan 02 jQuery
解决vue自定义指令导致的内存泄漏问题
Aug 04 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高级对象构建 工厂模式的使用
2012/02/05 PHP
php 模拟 asp.net webFrom 按钮提交事件的思路及代码
2013/12/02 PHP
php判断文件上传类型及过滤不安全数据的方法
2014/12/17 PHP
如何使用PHP给图片加水印
2016/10/12 PHP
php+lottery.js实现九宫格抽奖功能
2019/07/21 PHP
laravel利用中间件防止未登录用户直接访问后台的方法
2019/09/30 PHP
JS 判断undefined的实现代码
2009/11/26 Javascript
JavaScript 以对象为索引的关联数组
2010/05/19 Javascript
深入探究JavaScript中for循环的效率问题及相关优化
2016/03/13 Javascript
使用pcs api往免费的百度网盘上传下载文件的方法
2016/03/17 Javascript
Bootstrap table学习笔记(2) 前后端分页模糊查询
2017/05/18 Javascript
微信小程序实现商品属性联动选择
2019/02/15 Javascript
layui-tree实现Ajax异步请求后动态添加节点的方法
2019/09/23 Javascript
使用p5.js临摹动态图片
2019/11/04 Javascript
Vue中使用Lodop插件实现打印功能的简单方法
2019/12/19 Javascript
如何基于JavaScript判断图片是否加载完成
2019/12/28 Javascript
[01:07:47]Secret vs Optic Supermajor 胜者组 BO3 第一场 6.4
2018/06/05 DOTA
[01:07:13]TNC vs Pain 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
[02:58]魔廷新尊——痛苦女王至宝语音台词节选
2020/06/14 DOTA
python调用百度REST API实现语音识别
2018/08/30 Python
Python2和Python3中urllib库中urlencode的使用注意事项
2018/11/26 Python
在numpy矩阵中令小于0的元素改为0的实例
2019/01/26 Python
关于sys.stdout和print的区别详解
2019/12/05 Python
Pytorch 多块GPU的使用详解
2019/12/31 Python
Python实现自动访问网页的例子
2020/02/21 Python
Python的in,is和id函数代码实例
2020/04/18 Python
如何利用pycharm进行代码更新比较
2020/11/04 Python
纯CSS3实现漂亮的input输入框动画样式库(Text input love)
2018/12/29 HTML / CSS
世界上最大的售后摩托车零配件超市:J&P Cycles
2017/12/08 全球购物
简历中自我评价分享
2013/10/09 职场文书
篮球比赛策划方案
2014/06/05 职场文书
个人租房协议书
2014/11/28 职场文书
大学生求职自荐信范文
2015/03/04 职场文书
主婚人致辞精选
2015/07/28 职场文书
《用字母表示数》教学反思
2016/02/17 职场文书
Mysql 文件配置解析介绍
2022/05/06 MySQL