Angular動(dòng)態(tài)組件&響應(yīng)式表單的實(shí)現(xiàn)[附源碼]

本文將通過一個(gè)簡(jiǎn)單的例子簡(jiǎn)單展示Angular動(dòng)態(tài)組件+響應(yīng)式表單的使用。
【關(guān)鍵字】: Angular哎榴,動(dòng)態(tài)組件琼蚯,響應(yīng)式表單,ComponentFactoryResolver谋竖,ViewContainerRef红柱,ReactiveFormsModule
最終代碼在最后

  • 新建一個(gè)項(xiàng)目
ng new dynamic-form
  • 新建一個(gè)模塊(并在app.module中導(dǎo)入DynamicFormModule)
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
    declarations: [  ],
    imports: [
        CommonModule,
        ReactiveFormsModule,
    ],
    entryComponents: [    ],
    exports: [    ]
})
export class DynamicFormModule { }
  • 現(xiàn)在,我們需要?jiǎng)?chuàng)建用于創(chuàng)建動(dòng)態(tài)表單的容器
    動(dòng)態(tài)表單的入口點(diǎn)是主容器蓖乘。這將是我們的動(dòng)態(tài)表單模塊公開的唯一組件锤悄,負(fù)責(zé)接受表單配置并創(chuàng)建表單。
// dynamic-form-module components/dyanmic-form.component 
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FromItemConfig } from '../../models/dynamic-form.model';

@Component({
    selector: 'app-dynamic-form',
    templateUrl: './dynamic-form.component.html',
    styleUrls: ['./dynamic-form.component.scss']
})
export class DynamicFormComponent implements OnInit {
    @Input()
    public formConfigs: Array<FromItemConfig> = new Array<FromItemConfig>();
   
    @Output()
    public readonly submitted: EventEmitter<any> = new EventEmitter<any>();

    public formGroup: FormGroup;
    public constructor(private fb: FormBuilder ) {
this.formGroup = this.fb.group({});
    }

    public ngOnInit(): void {
        this.setFromControl();
    }

    public submitHandler(): void {
        // tslint:disable-next-line: forin
        for (const i in this.formGroup.controls) {
            this.formGroup.controls[i].markAsDirty();
            this.formGroup.controls[i].updateValueAndValidity();
        }
        if (this.formGroup.valid) {
            this.submitted.emit(this.formGroup.value);
        }
    }

    private setFromControl(): void {
        this.formConfigs?.forEach((control, idx) => {
            this.formGroup.addControl(control.controlName,
                this.fb.control(control.defaultValue || null, control?.required ? Validators.required : null));
        });
    }
}

由于我們的表單是動(dòng)態(tài)表單嘉抒,因此我們需要接受一個(gè)配置數(shù)組才能知道要?jiǎng)?chuàng)建什么零聚。為此,我們正在使用,@Input()它接受任何對(duì)象數(shù)組隶症。
對(duì)于配置中的每個(gè)項(xiàng)目政模,我們都希望該對(duì)象包含一些必要屬性,所以我們頂一個(gè)一個(gè)接口FromItemConfig

export interface FromItemConfig<T = any> {
    type: FromItemType;
    controlName: string;
    label: string;
    defaultValue?: T;
    disabled?: boolean;
    required?: boolean;
    options?: Array<OptionItem>;
}
export interface OptionItem {
    value: any;
    label: string;
}
// 這里根據(jù)我們已經(jīng)支持的表單控件類型定義了一個(gè)枚舉
export enum FromItemType {
    'checkbox' = 'checkbox',
    'input' = 'input',
    'number' = 'number',
    'textarea' = 'textarea',
    'radio' = 'radio',
    // ...
}
  • app.componet.html中使用我們創(chuàng)建的dynamic-form
<app-dynamic-form [formConfigs]="formConfigs" (submitted)="submitHandler($event)"></app-dynamic-form>
public formConfigs: Array<FromItemConfig> = [
  {
                type: FromItemType.input,
                controlName: 'name',
                label: '姓名',
                required: true
            },
            {
                type: FromItemType.radio,
                controlName: 'sex',
                label: '性別',
                defaultValue: 'man',
                options: [{ label: '男', value: 'man' }, { label: '女', value: 'woman' }, { label: '未知', value: 'unknown' }]
            },
            {
                type: FromItemType.input,
                controlName: 'nationality',
                label: '民族',
                defaultValue: '漢'
            },
            {
                type: FromItemType.number,
                controlName: 'age',
                label: '年齡',
            },
            {
                type: FromItemType.input,
                controlName: 'number',
                label: '學(xué)號(hào)',
            },
            {
                type: FromItemType.input,
                controlName: 'class',
                label: '班級(jí)',
            },
            {
                type: FromItemType.textarea,
                controlName: 'class',
                label: '愛好',
            },
            {
                type: FromItemType.checkbox,
                controlName: 'reConfirm',
                label: '已確認(rèn)',
                required: true
            }
]
  • 根據(jù)我們的需要去創(chuàng)建我們需要的表單項(xiàng)
// ***/dynamic-form/elements/
ng g c input
ng g c radio
...

這里以Input為例(這里我還用了ng-zorro)蚂会,其他的可以看源碼淋样。

<nz-form-item [formGroup]="formGroup">
  <nz-form-label [nzSpan]="6" nzFor="email" [nzRequired]="formConfig?.required">{{formConfig.label}}</nz-form-label>
  <nz-form-control [nzSpan]="14" nzErrorTip="該項(xiàng)為必填項(xiàng)">
    <input nz-input [placeholder]="formConfig.label" [formControlName]="formConfig.controlName"
      [name]="formConfig.controlName">
  </nz-form-control>
</nz-form-item>
import { Component, OnInit } from '@angular/core';
import { FormElementBaseClass } from '../../models/form-element-base.class';
import { FromItemConfig } from '../../models/dynamic-form.model';
import { FormGroup } from '@angular/forms';

@Component({
    selector: 'app-input',
    templateUrl: './input.component.html',
    styleUrls: ['./input.component.scss']
})
export class InputComponent {
    public formConfig: FromItemConfig;
    public formGroup: FormGroup;

    public get formControl(): { [key: string]: any } {
        return this.formGroup.controls;
    }
    public constructor() { }
}
  • 現(xiàn)在最關(guān)鍵的是dynamic-form.component 的實(shí)現(xiàn)
<div class="dynamic-form-container" (ngSubmit)="submitHandler()">
  <form class="dynamic-form" nz-form [formGroup]="formGroup">
    <ng-container *ngFor="let formConfig of formConfigs">
      <div appDynamicForm [formConfig]="formConfig" [formGroup]="formGroup"></div>
    </ng-container>

    <nz-form-item nz-row class="submit-area" *ngIf="formConfigs?.length">
      <nz-form-control [nzSpan]="14" [nzOffset]="6">
        <button nz-button nzType="primary" (click)="submitHandler()">Submit</button>
      </nz-form-control>
    </nz-form-item>
  </form>
</div>
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FromItemConfig } from '../../models/dynamic-form.model';

@Component({
    selector: 'app-dynamic-form',
    templateUrl: './dynamic-form.component.html',
    styleUrls: ['./dynamic-form.component.scss']
})
export class DynamicFormComponent implements OnInit {

    @Input()
    public formConfigs: Array<FromItemConfig> = new Array<FromItemConfig>();

    public formGroup: FormGroup;

    @Output()
    public readonly submitted: EventEmitter<any> = new EventEmitter<any>();

    public constructor(
        private fb: FormBuilder,
    ) {
        this.formGroup = this.fb.group({});
    }

    public ngOnInit(): void {
        this.setFromControl();
    }

    public submitHandler(): void {
        // tslint:disable-next-line: forin
        for (const i in this.formGroup.controls) {
            this.formGroup.controls[i].markAsDirty();
            this.formGroup.controls[i].updateValueAndValidity();
        }
        if (this.formGroup.valid) {
            this.submitted.emit(this.formGroup.value);
        }
    }
    private setFromControl(): void {
        this.formConfigs?.forEach((control, idx) => {
            this.formGroup.addControl(control.controlName,
                this.fb.control(control.defaultValue || null, control?.required ? Validators.required : null));
        });
    }
}
  • 從上面的代碼中,我們可以看到一個(gè)自定義指令 appDynamicForm胁住,這個(gè)自定義指令是最終實(shí)現(xiàn)的一個(gè)關(guān)鍵趁猴。 componentFactoryResolver(動(dòng)態(tài)組件加載器) 和 ViewContainerRef(視圖容器)是我們實(shí)現(xiàn)的根本。
import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Directive, Input, OnInit, ViewContainerRef } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { NumberComponent } from '../elements/number/number.component';
import { RadioGroupComponent } from '../elements/radio-group/radio-group.component';
import { FromItemConfig } from '../models/dynamic-form.model';
import { CheckboxComponent } from './../elements/checkbox/checkbox.component';
import { InputComponent } from './../elements/input/input.component';
import { TextareaComponent } from './../elements/textarea/textarea.component';

// 通過這個(gè)常量將類型和最終指向的組件對(duì)應(yīng)起來(lái)
const elementComponent: { [key: string]: any } = {
    checkbox: CheckboxComponent,
    input: InputComponent,
    textarea: TextareaComponent,
    radio: RadioGroupComponent,
    number: NumberComponent,
};

@Directive({
    selector: '[appDynamicForm]'
})
export class DynamicFormDirective implements OnInit {

    @Input()
    public formConfig: FromItemConfig;
    @Input()
    public formGroup: FormGroup;

    private component: ComponentRef<any>;
    public constructor(
        // ComponentFactoryResolver && ViewContainerRef
        private componentFactoryResolver: ComponentFactoryResolver,
        private viewContainerRef: ViewContainerRef,
    ) { }

    public ngOnInit(): void {
        this.viewContainerRef.clear();
        if (!this.formConfig) {
            return;
        }
        const component: any = elementComponent[this.formConfig.type];

        if (!component) {
            return;
        }

        const componentFactory: ComponentFactory<any> = this.componentFactoryResolver.resolveComponentFactory(component);
        const componentRef: ComponentRef<any> = this.viewContainerRef.createComponent(componentFactory);
        componentRef.instance.formConfig = this.formConfig;
        componentRef.instance.formGroup = this.formGroup;
    }
}

至此彪见,動(dòng)態(tài)響應(yīng)式表單的實(shí)現(xiàn)就基本完成了(過程講的比較粗略)儡司,有問題歡迎討論??
?? 查看所有代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市余指,隨后出現(xiàn)的幾起案子捕犬,更是在濱河造成了極大的恐慌,老刑警劉巖酵镜,帶你破解...
    沈念sama閱讀 212,686評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件或听,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡笋婿,警方通過查閱死者的電腦和手機(jī)誉裆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)缸濒,“玉大人足丢,你說(shuō)我怎么就攤上這事”优洌” “怎么了斩跌?”我有些...
    開封第一講書人閱讀 158,160評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捞慌。 經(jīng)常有香客問我耀鸦,道長(zhǎng),這世上最難降的妖魔是什么啸澡? 我笑而不...
    開封第一講書人閱讀 56,736評(píng)論 1 284
  • 正文 為了忘掉前任袖订,我火速辦了婚禮,結(jié)果婚禮上嗅虏,老公的妹妹穿的比我還像新娘洛姑。我一直安慰自己,他們只是感情好皮服,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,847評(píng)論 6 386
  • 文/花漫 我一把揭開白布楞艾。 她就那樣靜靜地躺著参咙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪硫眯。 梳的紋絲不亂的頭發(fā)上蕴侧,一...
    開封第一講書人閱讀 50,043評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音两入,去河邊找鬼净宵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛谆刨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播归斤,決...
    沈念sama閱讀 39,129評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼痊夭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了脏里?” 一聲冷哼從身側(cè)響起她我,我...
    開封第一講書人閱讀 37,872評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迫横,沒想到半個(gè)月后番舆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,318評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矾踱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,645評(píng)論 2 327
  • 正文 我和宋清朗相戀三年恨狈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呛讲。...
    茶點(diǎn)故事閱讀 38,777評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡禾怠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贝搁,到底是詐尸還是另有隱情吗氏,我是刑警寧澤,帶...
    沈念sama閱讀 34,470評(píng)論 4 333
  • 正文 年R本政府宣布雷逆,位于F島的核電站弦讽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏膀哲。R本人自食惡果不足惜往产,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望某宪。 院中可真熱鬧捂齐,春花似錦、人聲如沸缩抡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至压真,卻和暖如春娩嚼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滴肿。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工岳悟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泼差。 一個(gè)月前我還...
    沈念sama閱讀 46,589評(píng)論 2 362
  • 正文 我出身青樓贵少,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親堆缘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滔灶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,687評(píng)論 2 351

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