Angular2-基于動態(tài)創(chuàng)建組件的可配置表單設(shè)計(jì)

需求

  • 提供給管理員配置簡歷表單的功能甘桑。
  • 基本思路:將一個個表單項(xiàng)做成組件(例如輸入框,單選框歹叮,復(fù)選框跑杭,圖片,文件咆耿,日期等)德谅,每個組件對應(yīng)一段固定的json(包括組件名,placeholder萨螺,選項(xiàng)個數(shù)窄做,字?jǐn)?shù)限制等等),方便存儲屑迂。每增加一項(xiàng)就向json數(shù)組中增加一個元素浸策,然后把這段json存到后端。每個應(yīng)聘者的簡歷其實(shí)就是一份json惹盼,查看的時候根據(jù)這段json利用動態(tài)創(chuàng)建的方式渲染出來庸汗。

動態(tài)創(chuàng)建

? Angular提供了ComponentFactoryResolver,來協(xié)助我們在程序中動態(tài)產(chǎn)生不同的組件手报,而不用死板地把所有的組件都寫到view中去蚯舱,再根據(jù)條件判斷是否要顯示某個組件,當(dāng)遇到呈現(xiàn)的方式比較復(fù)雜的需求時非常好用掩蛤,寫出來的代碼也會簡潔枉昏,好看很多。例如可配置表單(簡歷揍鸟,問卷)兄裂,還有滾動廣告等。

開始之前阳藻,先介紹幾個對象

  1. ViewChild:一個屬性裝飾器晰奖,用來從模板視圖中獲取對應(yīng)的元素,可以通過模板變量獲取腥泥,獲取時可以通過 read 屬性設(shè)置查詢的條件匾南,就是說可以把此視圖轉(zhuǎn)為不同的實(shí)例。
  2. ViewContainerRef:一個視圖容器蛔外,可以在此上面創(chuàng)建蛆楞、插入溯乒、刪除組件等等。
  3. ComponentFactoryResolve:一個服務(wù)豹爹,動態(tài)加載組件的核心裆悄,這個服務(wù)可以將一個組件實(shí)例呈現(xiàn)到另一個組件視圖上。
  4. entryComponents:這個數(shù)組是用ViewContainerRef.createComponent()添加的動態(tài)添加的組件臂聋。將它們添加到entryComponents是告訴編譯器編譯它們并為它們創(chuàng)建Factory灯帮。路由配置中注冊的組件也自動添加到entryComponents,因?yàn)?code>router-outlet也使用ViewContainerRef.createComponent()將路由組件添加到DOM逻住。
  • 有了上面,一個簡單的思路便連貫了:特定區(qū)域就是一個視圖容器迎献,可以通過 ViewChild來實(shí)現(xiàn)獲取和查詢瞎访,然后使用ComponentFactoryResolve將已聲明未實(shí)例化的組件解析成為可以動態(tài)加載的 component,再將此component呈現(xiàn)到此前的視圖容器中吁恍。

1. 建立DynamicComponentDirective

? 首先先建立一個directive扒秸,并注入ViewContainerRefViewContainerRef是一個視圖容器冀瓦,可以在此上面創(chuàng)建伴奥、插入、刪除組件等等翼闽,代碼如下:

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

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

? 接著套用這個directive到需要動態(tài)加載的組件的容器上拾徙,簡單套用<ng-template>

  <!--動態(tài)產(chǎn)生組件的容器-->
  <ng-template dynamicComponent></ng-template>

2. 使用ComponentFactoryResolver動態(tài)產(chǎn)生組件

? 直接看代碼感局,都加了注釋尼啡。

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取得要動態(tài)放置Component的directive(componentHost)
  @ViewChild(DynamicComponentDirective) componentHost: DynamicComponentDirective;
  constructor(
    // 注入ComponentFactoryResolver
    private componentFactoryResolver: ComponentFactoryResolver
  ) { }
  title = '動態(tài)創(chuàng)建組件樣例';
  createNewComponent() {
    // 建立ComponentFactory
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(SampleComponent);
    const viewContainerRef = this.componentHost.viewContainerRef;
    // 產(chǎn)生我們需要的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>
  <!--動態(tài)產(chǎn)生組件的容器-->
  <ng-template dynamicComponent></ng-template>
  <button (click)="createNewComponent()">動態(tài)創(chuàng)建組件</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

有了以上知識鋪墊,可以進(jìn)入下一階段询微。

動態(tài)創(chuàng)建表單與表單的渲染

  • 要實(shí)現(xiàn)的效果如下:
  1. 動態(tài)創(chuàng)建一個組件崖瞭,生成一段json
  2. 根據(jù)json數(shù)組生成表單撑毛。

動態(tài)創(chuàng)建組件:

image.png

點(diǎn)擊保存模板书聚,保存json,根據(jù)json重新渲染表單:

image.png

具體步驟如下:

1. 以輸入框?yàn)槔宕疲xjson格式
// FormJson.ts
export class FormJson {
  public static basedata: any = {
    name : '',                   // 子項(xiàng)名稱
    id : '',
    hintText : '',               // 子選項(xiàng)提示
    type : '',                   // 組件類型
    numberLimit : '',            // 字?jǐn)?shù)限制
    content : [],                // 存放用戶填寫信息
  };
}
2. 創(chuàng)建可配置組件(以輸入框?yàn)槔?/h5>

執(zhí)行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() { }
  // 接收管理員配置的參數(shù)
  @Input() item: any;
  ngOnInit() {
  }
}
3. 動態(tài)創(chuàng)建表單

基本操作與動態(tài)創(chuàng)建組件是一樣的,每創(chuàng)建一個新的表單項(xiàng)蹦疑,formJson數(shù)組就增加一個元素西雀,這里存儲表單模板用的是json,所以渲染的時候要根據(jù)type來判斷要創(chuàng)建什么組件歉摧,當(dāng)然我這里只做了個輸入框艇肴,只做示例腔呜,其他的組件可以舉一反三。代碼解析如下:

// 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取得要動態(tài)放置Component的directive(componentHost)
  @ViewChild(DynamicComponentDirective) componentHost: DynamicComponentDirective;
  public baseData = JSON.parse(JSON.stringify(FormJson.basedata));
  public formJson = [];
  public save = false;          // 模擬根據(jù)后端json重新加載表單
  public formJsonText: string; // json文本
  constructor(
    // 注入ComponentFactoryResolver
    private componentFactoryResolver: ComponentFactoryResolver
  ) { }
  // 動態(tài)創(chuàng)建組件
  createInputComponent() {
    // 示例類型都是文本輸入框再悼,所以type字段都置為 text
    this.baseData.type = 'text';
    // 將json插入核畴,完成之后可存到后端
    this.formJson.push(this.baseData);
    // 頁面顯示json
    this.formJsonText = JSON.stringify(this.formJson);
    console.log(this.formJson);
    // 清除舊預(yù)覽
    this.componentHost.viewContainerRef.clear();
    // 渲染新示例頁面
    this.createForm(this.formJson);
    // 將json元素賦空,方便下次創(chuàng)建
    this.baseData = JSON.parse(JSON.stringify(FormJson.basedata));
  }
  // 根據(jù)json動態(tài)創(chuàng)建表單
  createForm(formJson) {
    const inputComponentFactory = this.componentFactoryResolver.resolveComponentFactory(InputComponent);
    // 遍歷json 根據(jù)不同類型創(chuàng)建組件冲九,可擴(kuò)充
    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;                  // 將管理員配置數(shù)據(jù)傳進(jìn)組件渲染
          break;
      }
    }
  }
  saveForm() {
    this.componentHost.viewContainerRef.clear();
    // todo 將表單模板存到后端
    console.log(this.formJson);
    this.save = true;
    setTimeout(() => {
      // todo 根據(jù)json重新解析,其實(shí)就像預(yù)覽一樣莺奸,調(diào)用createForm
      // 延時3s查看效果丑孩,便于理解
      this.createForm(this.formJson);
    }, 3000);
  }
}
<div style="width: 300px; float: left;height: 400px" align="center">
  <p>示例:創(chuàng)建自定義輸入框</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()">動態(tài)創(chuàng)建組件</button>
  </div>
</div>
<div style="width: 300px;margin-left: 500px;height: 400px" align="center">
  <div>
  <p *ngIf="save === false">示例:預(yù)覽</p>
    <div *ngIf="save">
      <p>--------JSON重新轉(zhuǎn)化為表單-----------</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>
操作演示:
  • 動態(tài)創(chuàng)建一個組件
image.png
  • 觀察預(yù)覽和son輸出
image.png
  • 點(diǎn)擊生成模板,根據(jù)json重新渲染組件
image.png

最后

源碼地址->Demo

在線示例->在線demo

? 這只是我個人對項(xiàng)目中的用法的總結(jié)灭贷,歡迎大家指正與交流温学。還有就是demo只是個demo,我并沒有花時間去做檢查甚疟、控制之類的仗岖,所以有bug很正常。览妖。

Reference

  1. reference -- 使用ComponentFactoryResolver動態(tài)產(chǎn)生Component
  2. reference -- angular2(4) 中動態(tài)創(chuàng)建組件的兩種方案
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末轧拄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讽膏,更是在濱河造成了極大的恐慌檩电,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件府树,死亡現(xiàn)場離奇詭異是嗜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)挺尾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門鹅搪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遭铺,你說我怎么就攤上這事丽柿。” “怎么了魂挂?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵甫题,是天一觀的道長。 經(jīng)常有香客問我涂召,道長坠非,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任果正,我火速辦了婚禮炎码,結(jié)果婚禮上盟迟,老公的妹妹穿的比我還像新娘。我一直安慰自己潦闲,他們只是感情好攒菠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歉闰,像睡著了一般辖众。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上和敬,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天凹炸,我揣著相機(jī)與錄音,去河邊找鬼昼弟。 笑死还惠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的私杜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼救欧,長吁一口氣:“原來是場噩夢啊……” “哼衰粹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笆怠,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铝耻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蹬刷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓢捉,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年办成,在試婚紗的時候發(fā)現(xiàn)自己被綠了泡态。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡迂卢,死狀恐怖某弦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情而克,我是刑警寧澤靶壮,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站员萍,受9級特大地震影響腾降,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碎绎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一螃壤、第九天 我趴在偏房一處隱蔽的房頂上張望抗果。 院中可真熱鬧,春花似錦映穗、人聲如沸窖张。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宿接。三九已至,卻和暖如春辕录,著一層夾襖步出監(jiān)牢的瞬間睦霎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工走诞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留副女,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓蚣旱,卻偏偏與公主長得像碑幅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子塞绿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容