2

1、什么是动态表单

动态表单指的是一个可以在运行时根据用户输入或其他动态因素改变的表单。与传统的静态表单不同,动态表单可以根据不同的条件、选项和输入值来改变它的外观和行为。

如果你有这样一种表单,其内容必须经常更改以满足快速变化的业务需求和监管需求,该技术就特别有用。一个典型的例子就是问卷。你可能需要在不同的上下文中获取用户的意见。用户要看到的表单格式和样式应该保持不变,而你要提的实际问题则会因上下文而异。

2、动态表单的构建思路

之前的表单项是写死的hard-coded,类似于如图的左侧,表单中的每一项都是写在代码里面的,如果想要修改表单项的类型和数量只能修改源代码;而动态表单中的每个表单项实际上是一个组件,其表单项是通过获得的数据动态渲染出来的,可以通过数据的不同来获得不同的表单项类型和数量,如果需要在某个地方动态的增加或者减少表单项,那么只需要通过修改数据就可以达到在某处增加组件(表单项)或者删除组件(表单项)的目的。

即动态表单的实现是通过一个数据对象数组,根据数据的不同个性化的渲染表单中的表单项,然后将所有的表单项封装到一个组件中,在需要使用动态表单的地方插入该组件的标签即可。

3、动态表单具体构建过程

3.1、引入ReactiveFormsModule

由于动态表单基于响应式表单,所以需要在动态表单所在模块引入ReactiveFormsModule,本文以在根模块创建为例。即在app.module.ts中引入该模块。


@NgModule({
  imports: [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule {
  constructor() {
  }
}

3.2、创建一个表单对象模型Form

创建Form类如下:

export class Form {
  key: string;        // 关键字
  value: string | undefined;    // 值    
  label: string;    // 标签内容
  order: number;    // 顺序
  controlType: string;        // 表单项类型(input, dropdown, custom...)
  type: string;        // 具体类型(input中的email, number, ...)
  options: {key: string, value: string}[];        // 子项(作用于controlType为dropdown, radio...时)
  rule = {} as {        // 验证规则
    required: boolean;
  }

  constructor(options: {
    value?: string;
    key?: string;
    label?: string;
    order?: number;
    controlType?: string;
    type?: string;
    options?: {key: string, value: string}[];
    rule?: {
      required: boolean
    }
  } = {}) {
    this.key = options.key || '';
    this.value = options.value;
    this.label = options.label || '';
    this.order = options.order === undefined ? 1 : options.order;
    this.controlType = options.controlType || '';
    this.type = options.type || '';
    this.options = options.options || [];
    this.rule = options.rule ? options.rule : {} as {required: boolean};
  }
}

3.3、创建动态表单模板

有了表单对象模型我们还需要创建一个放表单项的容器,这里我们创建一个动态表单模板组件DynamicFormComponent
dynamic-form.component.ts

import {Component, Input, OnInit} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Form} from '../Form';

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

  formGroup!: FormGroup
  @Input() forms: Form[] = [];

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(): void {
    console.log('onSubmit', this.formGroup.value);
  }
}

dynamic-form.component.html

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="formGroup">

    <div *ngFor="let form of forms" class="form-row">
      <!--表单项 [formGroup]="formGroup" [form]="form" -->
    </div>

    <div class="form-row">
      <button type="submit" [disabled]="!formGroup.valid">保存</button>
    </div>
  </form>
</div>

3.4、获取数据

现在我们已经有了Form类,以及放表单项的容器了,接下来我们创建服务类form.service.ts来获取数据,此处我们使用模拟数据,内容如下:

import { Injectable } from '@angular/core';
import {Observable, of} from 'rxjs';
import {Form} from '../Form';

@Injectable({
  providedIn: 'root'
})
export class FormService {

  constructor() { }

  getForms(): Observable<Form[]> {
    const forms: Form[] = [

      new Form({
        key: 'age',
        label: '年龄',
        controlType: 'select',
        options: [
          {key: '17', value: '18'},
          {key: '18', value: '19'},
          {key: '19', value: '20'},
        ],
        rule: {
          required: true
        }
      }),

      new Form({
        key: 'name',
        label: '姓名',
        controlType: 'textbox',
        type: 'text',
        rule: {
          required: true
        }
      }),

      new Form({
        key: 'phone',
        label: '手机号',
        controlType: 'textbox',
        type: 'number',
      })
    ];
    return of(forms.sort((a, b) => a.order - b.order));
  }
}


3.5、编写表单组

现在我们可以通过上面的服务类获取数据得到forms: Form[],接下来我们要实现通过forms转换得到formGroup: FormGroup。在dynamic-form.component.ts中,创建方法toFormGroup()来在组件通过ngOninit()初始化时将forms转换成formGroup(该方法也可以单独建立一个服务来实现),此时可以通过Form类的rule属性进行校验规则的设定。
修改完后dynamic-form.component.ts代码如下:

import {Component, Input, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {Form} from '../Form';

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

  formGroup!: FormGroup
  @Input() forms: Form[]|null = [];

  constructor() { }

  ngOnInit(): void {
    this.formGroup = this.toFormGroup(this.forms as Form[]);
  }

  onSubmit(): void {
    console.log('onSubmit', this.formGroup.value);
  }

  toFormGroup(forms: Form[] ) {
    const group: any = {};

    forms.forEach(form => {
      group[form.key] = form.rule.required ?
        new FormControl(form.value || '', Validators.required):
        new FormControl(form.value || '');
    });
    return new FormGroup(group);
  }
}

3.6、编写动态表单内容

既然已经有了容器,我们就可以放我们想要的内容了,也就是表单项,接下来我们创建一个组件DynamicFormUnitComponent,来使得每一个该组件实例可以匹配一个表单项。
dynamic-form-unit.component.ts

import {Component, Input, OnInit} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Form} from '../Form';

@Component({
  selector: 'app-dynamic-form-unit',
  templateUrl: './dynamic-form-unit.component.html',
})
export class DynamicFormUnitComponent implements OnInit {

  @Input() formGroup!: FormGroup;
  @Input() form!: Form;

  constructor() { }

  ngOnInit(): void {
  }

  get isValid() { return this.formGroup.controls[this.form.key].valid; }
}

dynamic-form-unit.component.html

<div [formGroup]="formGroup" [ngSwitch]="form.controlType">

  <ng-container *ngSwitchCase="'textbox'">
    <label [for]="form.key">{{ form.label }}</label>
    <input [formControlName]="form.key"
           [id]="form.key" [type]="form.type">
  </ng-container>

  <ng-container *ngSwitchCase="'dropdown'">
    <label [for]="form.key">{{ form.label }}</label>
    <select [formControlName]="form.key" [id]="form.key">
      <option *ngFor="let opt of form.options" [value]="opt.key"> {{ opt.value }}</option>
    </select>
  </ng-container>

  <ng-container *ngSwitchCase="'checkbox'">
    <label>
      {{ form.label }}
      <input type="checkbox" [name]="form.key" [formControlName]="form.key" [value]="form.value"/>
    </label>
  </ng-container>

  <ng-container *ngSwitchCase="'radio'">
    <h3>{{form.label}}</h3>
    <label *ngFor="let option of form.options">
      <input type="radio"
             [name]="option.key"
             [formControlName]="option.key"
             [value]="option.value"
      >
      {{option.key}}
    </label>
  </ng-container>

  <div style="color: red" *ngIf="!isValid">{{form.label}}不能为空</div>
</div>

该组件创建完成之后重新回到dynamic-form.component.html文件找到

<div *ngFor="let form of forms" class="form-row">
  <!--表单项 [formGroup]="formGroup" [form]="form" -->
</div>

修改成

<div *ngFor="let form of forms" class="form-row">
  <app-dynamic-form-unit [formGroup]="formGroup" [form]="form"></app-dynamic-form-unit>
</div>

此时动态表单就构建完成了,接下来我们去查看效果。

3.7、显示表单

要显示动态表单的一个实例,只需要在AppComponent中加入<app-dynamic-form>标签,AppComponent外壳模板会把一个 FormService 返回的 forms 数组传给表单容器组件 <app-dynamic-form>
app.component.ts

import { Component } from '@angular/core';
import {FormService} from './dynamic-form/service/form.service';
import {Observable} from 'rxjs';
import {Form} from './dynamic-form/Form';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>请输入相关信息</h2>
      <app-dynamic-form [forms]="forms$ | async"></app-dynamic-form>
    </div>`,
})
export class AppComponent {
  forms$: Observable<Form[]>;

  constructor(private formService: FormService) {
    this.forms$ = this.formService.getForms();
  }

}

效果图:

4、改进

4.1、构建组件

当前dynamic-form-unit.component.html的内容:

<div [formGroup]="formGroup" [ngSwitch]="form.controlType">

  <ng-container *ngSwitchCase="'textbox'">
    <label [for]="form.key">{{ form.label }}</label>
    <input [formControlName]="form.key"
           [id]="form.key" [type]="form.type">
  </ng-container>

  <ng-container *ngSwitchCase="'dropdown'">
    <label [for]="form.key">{{ form.label }}</label>
    <select [formControlName]="form.key" [id]="form.key">
      <option *ngFor="let opt of form.options" [value]="opt.key"> {{ opt.value }}</option>
    </select>
  </ng-container>

  <ng-container *ngSwitchCase="'checkbox'">
    <label>
      {{ form.label }}
      <input type="checkbox" [name]="form.key" [formControlName]="form.key" [value]="form.value"/>
    </label>
  </ng-container>

  <ng-container *ngSwitchCase="'radio'">
    <h3>{{form.label}}</h3>
    <label *ngFor="let option of form.options">
      <input type="radio"
             [name]="option.key"
             [formControlName]="option.key"
             [value]="option.value"
      >
      {{option.key}}
    </label>
  </ng-container>

  <div style="color: red" *ngIf="!isValid">{{form.label}}不能为空</div>
</div>

可以看到该模板文件中每一个<ng-container></ng-container>中对应了一种表单项类型,我们对每一项构建一个组件从而方便代码的维护,也方便代码测试以及重用构建不同的模板。在此以DynamicTextboxComponent为例给出代码,其他组件类似。
dynamic-textbox.component.ts

import {Component, Input, OnInit} from '@angular/core';
import {Form} from '../../../Form';
import {FormGroup} from '@angular/forms';

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

  @Input() form!: Form;
  @Input() formGroup!: FormGroup;

  constructor() { }

  ngOnInit(): void {
  }

}

dynamic-textbox.component.html

<div [formGroup]="formGroup">
  <label>
    {{ form.label }}
    <input type="checkbox" [name]="form.key" [formControlName]="form.key" [value]="form.value"/>
  </label>
</div>

对所有表单项创建完组件后,dynamic-form.component.html代码如下:

<div [formGroup]="formGroup" [ngSwitch]="form.controlType">
  <app-dynamic-textbox *ngSwitchCase="'textbox'" [formGroup]="formGroup" [form]="form"></app-dynamic-textbox>
  <app-dynamic-dropdown *ngSwitchCase="'dropdown'" [formGroup]="formGroup" [form]="form"></app-dynamic-dropdown>
  <app-dynamic-checkbox *ngSwitchCase="'checkbox'" [formGroup]="formGroup" [form]="form"></app-dynamic-checkbox>
  <app-dynamic-radio *ngSwitchCase="'radio'" [formGroup]="formGroup" [form]="form"></app-dynamic-radio>
  <div style="color: red" *ngIf="!isValid">{{form.label}}不能为空</div>
</div>

4.2、取消@Input()装饰器

为简化代码,可以删除掉每个组件的formGroup属性的@Input()装饰器,使用Angular自身的FormGroupDirective来维持各表单项的联系。同样以DynamicTextComponent为例给出代码。
dynamic-textbox.component.ts

import {Component, Input, OnInit} from '@angular/core';
import {Form} from '../../../Form';
import {FormGroup, FormGroupDirective} from '@angular/forms';

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

  @Input() form!: Form;
  formGroup!: FormGroup;

  constructor(private fgDirective: FormGroupDirective) { }

  ngOnInit(): void {
    this.formGroup = this.fgDirective.control;
  }

}

dynamic-textbox.component.html

<div [formGroup]="formGroup">
  <label [for]="form.key">{{ form.label }}</label>
  <input [formControlName]="form.key"
         [id]="form.key" [type]="form.type">
</div>

对四个组件都进行如上操作后,文件
dynamic-form-unit.component.html

<div [formGroup]="formGroup" [ngSwitch]="form.controlType">
  <app-dynamic-textbox *ngSwitchCase="'textbox'" [form]="form"></app-dynamic-textbox>
  <app-dynamic-dropdown *ngSwitchCase="'dropdown'" [form]="form"></app-dynamic-dropdown>
  <app-dynamic-checkbox *ngSwitchCase="'checkbox'" [form]="form"></app-dynamic-checkbox>
  <app-dynamic-radio *ngSwitchCase="'radio'" [form]="form"></app-dynamic-radio>
  <div style="color: red" *ngIf="!isValid">{{form.label}}不能为空</div>
</div>

另外,DynamicFormUnitComponent组件也可以进行如上修改,修改后
dynamic-form.component.html

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="formGroup">

    <div *ngFor="let form of forms" class="form-row">
      <app-dynamic-form-unit [form]="form"></app-dynamic-form-unit>
    </div>

    <div class="form-row">
      <button type="submit" [disabled]="!formGroup.valid">保存</button>
    </div>
  </form>
</div>

5、总结

以上是动态表单的基本的构建过程以及一些简化方式,除此之外,其实还有使用类似动态组件的方式替换*ngSwitch的控制方式,动态表单FormArray的使用,以及控制表单项前后数据的关联等一些其他内容,本文不再继续讨论这些内容,具体可以参考以下文章

  1. https://angular.cn/guide/dynamic-form#display-the-form
  2. https://www.danywalls.com/creating-dynamic-forms-in-angular-a...

code:https://github.com/chshihang/dynamic-form


chshihang
124 声望13 粉丝