美文网首页angular 2+ 点点滴滴让前端飞前端攻城狮
Angular2-基于动态创建组件的可配置表单设计

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

作者: Husbin | 来源:发表于2018-02-05 01:18 被阅读147次

    需求

    • 提供给管理员配置简历表单的功能。
    • 基本思路:将一个个表单项做成组件(例如输入框,单选框,复选框,图片,文件,日期等),每个组件对应一段固定的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) 中动态创建组件的两种方案

    相关文章

      网友评论

        本文标题:Angular2-基于动态创建组件的可配置表单设计

        本文链接:https://www.haomeiwen.com/subject/tzovzxtx.html