Angular2-基于动态创建组件的可配置表单设计

需求

  • 提供给管理员配置简历表单的功能。
  • 基本思路:将一个个表单项做成组件(例如输入框,单选框,复选框,图片,文件,日期等),每个组件对应一段固定的json(包括组件名,placeholder,选项个数,字数限制等等),方便存储。每增加一项就向json数组中增加一个元素,然后把这段json存到后端。每个应聘者的简历其实就是一份json,查看的时候根据这段json利用动态创建的方式渲染出来。

动态创建

​ Angular提供了ComponentFactoryResolver,来协助我们在程序中动态产生不同的组件,而不用死板地把所有的组件都写到view中去,再根据条件判断是否要显示某个组件,当遇到呈现的方式比较复杂的需求时非常好用,写出来的代码也会简洁,好看很多。例如可配置表单(简历,问卷),还有滚动广告等。

开始之前,先介绍几个对象

  1. ViewChild:一个属性装饰器,用来从模板视图中获取对应的元素,可以通过模板变量获取,获取时可以通过 read 属性设置查询的条件,就是说可以把此视图转为不同的实例。
  2. ViewContainerRef:一个视图容器,可以在此上面创建、插入、删除组件等等。
  3. ComponentFactoryResolve:一个服务,动态加载组件的核心,这个服务可以将一个组件实例呈现到另一个组件视图上。
  4. entryComponents:这个数组是用ViewContainerRef.createComponent()添加的动态添加的组件。将它们添加到entryComponents是告诉编译器编译它们并为它们创建Factory。路由配置中注册的组件也自动添加到entryComponents,因为router-outlet也使用ViewContainerRef.createComponent()将路由组件添加到DOM。
  • 有了上面,一个简单的思路便连贯了:特定区域就是一个视图容器,可以通过 ViewChild来实现获取和查询,然后使用ComponentFactoryResolve将已声明未实例化的组件解析成为可以动态加载的 component,再将此component呈现到此前的视图容器中。

1. 建立DynamicComponentDirective

​ 首先先建立一个directive,并注入ViewContainerRefViewContainerRef是一个视图容器,可以在此上面创建、插入、删除组件等等,代码如下:

// DynamicComponentDirective.ts
import {Directive, ViewContainerRef} from '@angular/core';

@Directive({
  selector: '[dynamicComponent]'
})
export class DynamicComponentDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

​ 接着套用这个directive到需要动态加载的组件的容器上,简单套用<ng-template>

  <!--动态产生组件的容器-->
  <ng-template dynamicComponent></ng-template>

2. 使用ComponentFactoryResolver动态产生组件

​ 直接看代码,都加了注释。

import {Component, ComponentFactoryResolver, ViewChild} from '@angular/core';
import {DynamicComponentDirective} from './DynamicComponentDirective';
import {SampleComponent} from './sample/sample.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  // 使用ViewChild取得要动态放置Component的directive(componentHost)
  @ViewChild(DynamicComponentDirective) componentHost: DynamicComponentDirective;
  constructor(
    // 注入ComponentFactoryResolver
    private componentFactoryResolver: ComponentFactoryResolver
  ) { }
  title = '动态创建组件样例';
  createNewComponent() {
    // 建立ComponentFactory
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(SampleComponent);
    const viewContainerRef = this.componentHost.viewContainerRef;
    // 产生我们需要的Component并放入componentHost之中
    viewContainerRef.createComponent(componentFactory);
    // const componentRef = viewContainerRef.createComponent(componentFactory);
  }
  clearView() {
    const viewContainerRef = this.componentHost.viewContainerRef;
    viewContainerRef.clear();
  }
}
<div style="text-align:center">
  <h1>
    {{ title }}
  </h1>
  <!--动态产生组件的容器-->
  <ng-template dynamicComponent></ng-template>
  <button (click)="createNewComponent()">动态创建组件</button>
  <button (click)="clearView()">清除视图</button>
</div>

3. 在Module中加入entryComponents

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { SampleComponent } from './sample/sample.component';
import {DynamicComponentDirective} from './DynamicComponentDirective';
@NgModule({
  declarations: [
    AppComponent,
    SampleComponent,
    DynamicComponentDirective
  ],
  imports: [
    BrowserModule
  ],
  entryComponents: [
    SampleComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

4. 效果展示

image.png

有了以上知识铺垫,可以进入下一阶段。

动态创建表单与表单的渲染

  • 要实现的效果如下:
  1. 动态创建一个组件,生成一段json
  2. 根据json数组生成表单。

动态创建组件:

image.png

点击保存模板,保存json,根据json重新渲染表单:

image.png

具体步骤如下:

1. 以输入框为例,定义json格式
// FormJson.ts
export class FormJson {
  public static basedata: any = {
    name : '',                   // 子项名称
    id : '',
    hintText : '',               // 子选项提示
    type : '',                   // 组件类型
    numberLimit : '',            // 字数限制
    content : [],                // 存放用户填写信息
  };
}
2. 创建可配置组件(以输入框为例)

执行ng -g component input新建一个组件,代码如下:

<!--input.component.html-->
<!--显示模板-->
<div>
  <label for="input">{{item.name}}</label>
  <input
    id="input"
    [(ngModel)]="item.content"
    type="{{item.type}}"
    placeholder="{{item.hintText}}"
  >
</div>
//input.component.ts
import {Component, Input, OnInit} from '@angular/core';
@Component({
  selector: 'app-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.css']
})
export class InputComponent implements OnInit {
  constructor() { }
  // 接收管理员配置的参数
  @Input() item: any;
  ngOnInit() {
  }
}
3. 动态创建表单

基本操作与动态创建组件是一样的,每创建一个新的表单项,formJson数组就增加一个元素,这里存储表单模板用的是json,所以渲染的时候要根据type来判断要创建什么组件,当然我这里只做了个输入框,只做示例,其他的组件可以举一反三。代码解析如下:

// app.component.ts
import {Component, ComponentFactoryResolver, ViewChild} from '@angular/core';
import {DynamicComponentDirective} from './share/DynamicComponentDirective';
import {SampleComponent} from './sample/sample.component';
import {FormJson} from './share/FormJson';
import {InputComponent} from './input/input.component';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  // 使用ViewChild取得要动态放置Component的directive(componentHost)
  @ViewChild(DynamicComponentDirective) componentHost: DynamicComponentDirective;
  public baseData = JSON.parse(JSON.stringify(FormJson.basedata));
  public formJson = [];
  public save = false;          // 模拟根据后端json重新加载表单
  public formJsonText: string; // json文本
  constructor(
    // 注入ComponentFactoryResolver
    private componentFactoryResolver: ComponentFactoryResolver
  ) { }
  // 动态创建组件
  createInputComponent() {
    // 示例类型都是文本输入框,所以type字段都置为 text
    this.baseData.type = 'text';
    // 将json插入,完成之后可存到后端
    this.formJson.push(this.baseData);
    // 页面显示json
    this.formJsonText = JSON.stringify(this.formJson);
    console.log(this.formJson);
    // 清除旧预览
    this.componentHost.viewContainerRef.clear();
    // 渲染新示例页面
    this.createForm(this.formJson);
    // 将json元素赋空,方便下次创建
    this.baseData = JSON.parse(JSON.stringify(FormJson.basedata));
  }
  // 根据json动态创建表单
  createForm(formJson) {
    const inputComponentFactory = this.componentFactoryResolver.resolveComponentFactory(InputComponent);
    // 遍历json 根据不同类型创建组件,可扩充
    for (let i = 0 ; i < formJson.length ; i++) {
      const item = formJson[i] ;
      let componentRef;
      switch (item.type) {
        case 'text':
          componentRef = this.componentHost.viewContainerRef.createComponent(inputComponentFactory);
          componentRef.instance.componentRef = componentRef;  // 传入自身组件引用,用于返回来编辑自身
          componentRef.instance.item = item;                  // 将管理员配置数据传进组件渲染
          break;
      }
    }
  }
  saveForm() {
    this.componentHost.viewContainerRef.clear();
    // todo 将表单模板存到后端
    console.log(this.formJson);
    this.save = true;
    setTimeout(() => {
      // todo 根据json重新解析,其实就像预览一样,调用createForm
      // 延时3s查看效果,便于理解
      this.createForm(this.formJson);
    }, 3000);
  }
}
<div style="width: 300px; float: left;height: 400px" align="center">
  <p>示例:创建自定义输入框</p>
  <div>
  <label for="name">输入框名字:</label>
    <input
      id="name"
      [(ngModel)]="baseData.name"
    >
  </div>
  <div>
      <label for="placeholder">输入框提示:</label>
      <input
        id="placeholder"
        [(ngModel)]="baseData.hintText"
      >
    </div>
  <div align="center" style="margin: 10px">
  <button (click)="createInputComponent()">动态创建组件</button>
  </div>
</div>
<div style="width: 300px;margin-left: 500px;height: 400px" align="center">
  <div>
  <p *ngIf="save === false">示例:预览</p>
    <div *ngIf="save">
      <p>--------JSON重新转化为表单-----------</p>
      <p>--------延时让效果更明显-----------</p>
    </div>
  <ng-template dynamicComponent></ng-template>
  </div>
  <div style="margin: 10px">
  <button (click)="saveForm()">保存模板</button>
  </div>
</div>
<div style="width: 500px;height: 400px" align="center">
  <div>
    <p >打印JSON</p>
  </div>
  <div style="margin: 10px">
    <textarea [(ngModel)] = "formJsonText" rows="30" style="width: 500px"></textarea>
  </div>
</div>
操作演示:
  • 动态创建一个组件
image.png
  • 观察预览和son输出
image.png
  • 点击生成模板,根据json重新渲染组件
image.png

最后

源码地址->Demo

在线示例->在线demo

​ 这只是我个人对项目中的用法的总结,欢迎大家指正与交流。还有就是demo只是个demo,我并没有花时间去做检查、控制之类的,所以有bug很正常。。

Reference

  1. reference -- 使用ComponentFactoryResolver动态产生Component
  2. reference -- angular2(4) 中动态创建组件的两种方案
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容