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 支持链式调用的异步调用框架Async.Operation
Aug 04 Javascript
仅IE6/7/8中innerHTML返回值忽略英文空格的问题
Apr 07 Javascript
JS 如果改变span标签的是否隐藏属性
Oct 06 Javascript
关于jQuery的inArray 方法介绍
Oct 08 Javascript
滚动图片效果 jquery实现回旋滚动效果
Jan 08 Javascript
简述JavaScript中正则表达式的使用方法
Jun 15 Javascript
BootStrap响应式导航条实例介绍
May 06 Javascript
JavaScript实现简单的日历效果
Sep 25 Javascript
mpvue将vue项目转换为小程序
Sep 30 Javascript
node express使用HTML模板的方法示例
Aug 22 Javascript
Vue.js中provide/inject实现响应式数据更新的方法示例
Oct 16 Javascript
jQuery开发仿QQ版音乐播放器
Jul 10 jQuery
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
解决了Ajax、MySQL 和 Zend Framework 的乱码问题
2009/03/03 PHP
分享一个超好用的php header下载函数
2014/01/31 PHP
ThinkPHP之A方法实例讲解
2014/06/20 PHP
PHP判断是否连接上网络的方法
2015/07/01 PHP
php实现将Session写入数据库
2015/07/26 PHP
php使用redis的几种常见操作方式和用法示例
2020/02/20 PHP
实现JavaScript中继承的三种方式
2009/10/16 Javascript
菜鸟学习JavaScript小实验之函数引用
2010/11/17 Javascript
在JavaScript中监听IME键盘输入事件
2011/05/29 Javascript
Javascript判断文件是否存在(客户端/服务器端)
2014/09/16 Javascript
jQuery中使用each处理json数据
2015/04/23 Javascript
js实现点击链接后延迟3秒再跳转的方法
2015/06/05 Javascript
Jquery插件easyUi实现表单验证示例
2015/12/15 Javascript
小白谈谈对JS原型链的理解
2016/05/03 Javascript
jqGrid翻页时数据选中丢失问题的解决办法
2017/02/13 Javascript
jQuery插件FusionWidgets实现的Bulb图效果示例【附demo源码下载】
2017/03/23 jQuery
微信小程序动态的加载数据实例代码
2017/04/14 Javascript
Node.js中流(stream)的使用方法示例
2017/07/16 Javascript
JavaScript实现缓动动画
2020/11/25 Javascript
Java中重定向输出流实现用文件记录程序日志
2015/06/12 Python
windows 下python+numpy安装实用教程
2017/12/23 Python
利用Python实现原创工具的Logo与Help
2018/12/03 Python
python笔记之mean()函数实现求取均值的功能代码
2019/07/05 Python
在Python中画图(基于Jupyter notebook的魔法函数)
2019/10/28 Python
python使用Thread的setDaemon启动后台线程教程
2020/04/25 Python
使用Html5、CSS实现文字阴影效果
2018/01/17 HTML / CSS
空乘英文求职信
2014/04/13 职场文书
机关保密承诺书
2014/06/03 职场文书
校车安全责任书
2014/08/25 职场文书
公司收款委托书范本
2014/09/20 职场文书
学生检讨书范文
2015/01/27 职场文书
2015年大学生村官工作总结
2015/04/21 职场文书
小学思品教学反思
2016/02/20 职场文书
创业不要错过,这4种餐饮新模式
2019/07/18 职场文书
Python图片检索之以图搜图
2021/05/31 Python
搭建zabbix监控以及邮件报警的超级详细教学
2022/07/15 Servers