Angular动态组件&响应式表单的实现[附源码]

本文将通过一个简单的例子简单展示Angular动态组件+响应式表单的使用。
【关键字】: Angular,动态组件,响应式表单,ComponentFactoryResolver,ViewContainerRef,ReactiveFormsModule
最终代码在最后

  • 新建一个项目
ng new dynamic-form
  • 新建一个模块(并在app.module中导入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 { }
  • 现在,我们需要创建用于创建动态表单的容器
    动态表单的入口点是主容器。这将是我们的动态表单模块公开的唯一组件,负责接受表单配置并创建表单。
// 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));
        });
    }
}

由于我们的表单是动态表单,因此我们需要接受一个配置数组才能知道要创建什么。为此,我们正在使用,@Input()它接受任何对象数组。
对于配置中的每个项目,我们都希望该对象包含一些必要属性,所以我们顶一个一个接口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;
}
// 这里根据我们已经支持的表单控件类型定义了一个枚举
export enum FromItemType {
    'checkbox' = 'checkbox',
    'input' = 'input',
    'number' = 'number',
    'textarea' = 'textarea',
    'radio' = 'radio',
    // ...
}
  • app.componet.html中使用我们创建的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: '学号',
            },
            {
                type: FromItemType.input,
                controlName: 'class',
                label: '班级',
            },
            {
                type: FromItemType.textarea,
                controlName: 'class',
                label: '爱好',
            },
            {
                type: FromItemType.checkbox,
                controlName: 'reConfirm',
                label: '已确认',
                required: true
            }
]
  • 根据我们的需要去创建我们需要的表单项
// ***/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="该项为必填项">
    <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() { }
}
  • 现在最关键的是dynamic-form.component 的实现
<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));
        });
    }
}
  • 从上面的代码中,我们可以看到一个自定义指令 appDynamicForm,这个自定义指令是最终实现的一个关键。 componentFactoryResolver(动态组件加载器) 和 ViewContainerRef(视图容器)是我们实现的根本。
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';

// 通过这个常量将类型和最终指向的组件对应起来
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;
    }
}

至此,动态响应式表单的实现就基本完成了(过程讲的比较粗略),有问题欢迎讨论😊
🤞 查看所有代码

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

推荐阅读更多精彩内容