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

在阅读本文之前,请确保你已经掌握 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;
}

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

Angular 4.x 动态创建表单实例的更多相关文章

  1. Android Studio是否支持用于Android UI设计的AngularJS?

    我对AndroidStudio有疑问:AS在设计XML文件时是否支持AngularJS代码,例如:对于小动画或效果?

  2. android – 如何使用ClientID和ClientSecret在Phonegap中使用Angularjs登录Google OAuth2

    我正尝试使用Angularjs(使用IonicFramework)通过GoogleOAuth2从我的Phonegap应用程序登录.目前我正在使用http://phonegap-tips.com/articles/google-api-oauth-with-phonegaps-inappbrowser.html进行登录.但是当我使用Angular-UI-RouterforIonic时,它正在创建非常

  3. 利用require.js与angular搭建spa应用的方法实例

    这篇文章主要给大家介绍了关于利用require.js与angular搭建spa应用的方法实例,文中通过示例代码给大家介绍的非常详细,对大家的理解和学习具有一定的参考学习价值,需要的朋友们下面跟着小编来一起看看吧。

  4. 详解Angular动态组件

    本文主要介绍了Angular动态组件,对此感兴趣的同学,可以亲自实验一下。

  5. 详解如何使用webpack+es6开发angular1.x

    本篇文章主要介绍了详解如何使用webpack+es6开发angular1.x,具有一定的参考价值,有兴趣的可以了解一下

  6. angular2系列之路由转场动画的示例代码

    本篇文章主要介绍了angular2系列之路由转场动画的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. 一种angular的方法级的缓存注解(装饰器)

    本篇文章主要介绍了一种angular的方法级的缓存注解(装饰器),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  8. 动手写一个angular版本的Message组件的方法

    本篇文章主要介绍了动手写一个angular版本的Message组件的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  9. angular forEach方法遍历源码解读

    这篇文章主要为大家详细了angular forEach方法遍历源码,forEach()方法用于遍历对象或数组,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Angular的MVC和作用域

    本文主要Angular的MVC和作用域进行详细分析介绍,具有一定的参考价值,下面跟着小编一起来看下吧

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部