Angular Form-动态表单

前言

动态表单是基于响应式表单的。所以需要提前准备相关知识。本示例参考了官方的教程,在官方教程的基础上进行了拓展。可以给每个表单项添加多个校验。

动态表单

这里会做一个简单的表单,包括 input 输入框、下拉框、单选按钮。

结构

├── app
│ ├── dynamic-from
│ │ ├── shared
│ │ │  ├── dynamic-base.ts
│ │ │  ├── dynamic-text.ts
│ │ │  ├── dynamic-select.ts
│ │ │  ├── dynamic-radio.ts
│ │ │  ├── dynamic-form-group.service.ts
│ │ │  └── dynamic-from-data.service.ts
│ │ ├── dynamic-from-item
│ │ │  ├── dynamic-from-item.component.ts
│ │ │  └── dynamic-from-item.component.html
│ │ ├── dynamic-from.component.ts
│ │ └── dynamic-from.component.html
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app-component.ts
│ └── app.module.ts

样式文件就省去了

代码演示

在 app.module.ts 中引入相关的组件及服务供全局使用。

<!-- app.module.ts  -->
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
import { DynamicFormItemComponent } from "./dynamic-form/form-item/dynamic-form-item.component";
import { DynamicFormDataService } from './dynamic-form/shared/dynamic-form-data.service';
import { DynamicFormGroupService } from './dynamic-form/shared/dynamic-form-group.service';

@NgModule({
  declarations: [
    AppComponent,
    DynamicFormComponent,
    DynamicFormItemComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],
  providers: [DynamicFormDataService, DynamicFormGroupService],
  bootstrap: [AppComponent]
})
export class AppModule { }

在 app.component.ts 中引入表单组件。

// app.component.ts
import { Component } from '@angular/core';
import { DynamicFormDataService } from './dynamic-form/shared/dynamic-form-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {
  formItems: any[]
  constructor(private dynamicFormDataService: DynamicFormDataService) {
    this.formItems = dynamicFormDataService.getFormData();
  }
}

app.component.html

<dynamic-form [formItems]="formItems"></dynamic-form>

在 dynamic-form.component.ts 中引入表单里引入 form 标签并在这里通过数据遍历表单项。

// dynamic-form/dynamic-form.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicBase } from "./shared/dynamic-base"
import { DynamicFormGroupService } from './shared/dynamic-form-group.service';
@Component({
  selector: 'dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.less']
})
export class DynamicFormComponent implements OnInit {
  @Input() formItems: DynamicBase<any>[]
  @Input() dynamicFormGroup: FormGroup
  payLoad = ''
  // 注入 DynamicFormGroupService 服务
  constructor(private dynamicFormGroupService: DynamicFormGroupService) { }
  ngOnInit() {
    // 通过 DynamicFormGroupService 来创建一个 FormGroup
    this.dynamicFormGroup = this.dynamicFormGroupService.getFormGroup(this.formItems)
  }
  save() {
    this.payLoad = JSON.stringify(this.dynamicFormGroup.value);
    console.log(this.payLoad)
  }
}

dynamic-form/dynamic-form.component.html

<form [formGroup]="dynamicFormGroup" (ngSubmit)="save()">
  <div *ngFor="let formItem of formItems">
    <dynamic-form-item [formItem]="formItem" [dynamicFormGroup]="dynamicFormGroup"></dynamic-form-item>
  </div>
  <button type="submit" *ngIf="dynamicFormGroup.valid">保存</button>
</form>

在 dynamic-form.component-item.ts 中引入表单项组件,这个表单项里包含了多种类型,input、select、radio。表单项组件会根据数据来选择性的显示是 input 还是 select ,或者是 radio。

如果表单全部通过校验,那么就会显示保存按钮。

我们不难看出所有的操作,都是基于我们在 form 标签上所定义的 [formGroup] 属性这个变量来进行操作。表单中所涉及到的相关数据都会挂到这个对象下。比如:整个表单是否通过验证,每个表单项值的有效性,以及整个表单的输入数据等。

// dynamic-form/dynamic-item/dynamic-form-item.component.ts
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicBase } from "../shared/dynamic-base"
@Component({
  selector: 'dynamic-form-item',
  templateUrl: './dynamic-form-item.component.html',
  styleUrls: ['./dynamic-form-item.component.less']
})
export class DynamicFormItemComponent {
  @Input() formItem: DynamicBase<any>
  @Input() dynamicFormGroup: FormGroup
  constructor() { }
  get isValid() {
    return this.dynamicFormGroup.controls[this.formItem.key].valid
  }
  get currentError() {
    const keys = this.dynamicFormGroup.controls[this.formItem.key].errors
    console.log(keys);
    const validators = this.formItem.validators;
    let msg = ''
    if (validators) {
      for (let key in keys) {
        for (let item of validators) {
          if (item.type.toLowerCase() === key) {
            msg = item.msg
            return msg
          }
        }
      }
    }
  }
}

上面:

this.dynamicFormGroup.controls[this.formItem.key].errors 这个是用来拿到对应表单项的错误信息的,它是一个对象,其中包含的属性有可能是一个,也有可能是多个。

this.dynamicFormGroup.controls[this.formItem.key].valid 用来取得对应表单项是否有效(是否通过校验)

我们定义了两个 get 属性,我们可以把 get 属性理解成 vue 中的 computed 属性。

其中我们通过 [checked] 这种绑定方式来设置单选按钮的默认值。

dynamic-form/dynamic-item/dynamic-form-item.component.html

<div [formGroup]="dynamicFormGroup">
  <label [attr.for]="formItem.key">{{formItem.label}}</label>
  <div [ngSwitch]="formItem.controlType">
    <input *ngSwitchCase="'text'" [formControlName]="formItem.key" [id]="formItem.key" [type]="formItem.type">
    <select [id]="formItem.key" *ngSwitchCase="'select'" [formControlName]="formItem.key">
      <option *ngFor="let opt of formItem.options" [value]="opt.key">{{opt.value}}</option>
    </select>
    <div *ngSwitchCase="'radio'">
      <label [attr.for]="rad.key" *ngFor="let rad of formItem.items">
        {{rad.label}}
        <input [name]="formItem.key" [checked]="formItem.value === rad.value" [formControlName]="formItem.key" [id]="formItem.key"
          [type]="formItem.type" [value]="rad.value">
      </label>
    </div>
  </div>
  <div *ngIf="!isValid">{{currentError}}</div>
</div>

上面就是表单项的类别,根据不同的配置类型显示不同的项,需要注意的是我们向给最外层 div 添加 [formGroup]="dynamicFormGroup",不然会报错。

下面定义一个基类 DynamicBase,这个基类的好处就是后面用到的 input 类、select 类 和 radio 类都可以继承这个类,而不需要第个类都得重复相同的代码字义,并第个子类都可以作相应的拓展。

// dynamic-form/shared/dynamic-base.ts
interface validator {
  type: string,
  msg: string,
  param?: any
}
export class DynamicBase<T> {
  value: T;
  key: string;
  label: string;
  required: boolean;
  order: number;
  controlType: string;
  validators: validator[]

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

下面三个 .ts 文件就是分别定义三个不同的表单项类

DynamicText 类

// dynamic-form/shared/dynamic-text.ts
import { DynamicBase } from "./dynamic-base"
export class DynamicText extends DynamicBase<string> {
  controlType = 'text'
  type: string;
  constructor(options: {} = {}) {
    // 继承 DynamicBase 类
    super(options)
    // 追加输入框的类型
    this.type = options['type'] || '';
  }
}

DynamicSelect  类

dynamic-form/shared/dynamic-select.ts
import { DynamicBase } from "./dynamic-base"
export class DynamicSelect extends DynamicBase<number|string> {
  controlType = 'select'
  options: { key: string, value: string }[];
  constructor(options: {} = {}) {
    // 继承 DynamicBase 类
    super(options)
    // 追加下拉框的 options 属性
    this.options = options['options'] || []
  }
}

DynamicRadio 类

// dynamic-form/shared/dynamic-radio.ts
import { DynamicBase } from "./dynamic-base"
export class DynamicRadio extends DynamicBase<number> {
  controlType = 'radio'
  type: string;
  items: { label: string, value: number }[];
  constructor(options: {} = {}) {
    // 继承 DynamicBase 类
    super(options)
    // 追加输入框的类型
    this.type = options['type'] || '';
    this.items = options['items'] || [];
  }
}

下面定义了一个用来生成响应式表单的服务

// dynamic-form/shared/dynamic-form-group.service.ts
import { Injectable, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DynamicBase } from "./dynamic-base"

@Injectable()
export class DynamicFormGroupService {
  constructor() { }
  getFormGroup(dynamics: DynamicBase<any>[]) {
    let group: any = {};
    dynamics.forEach(dynamic => {
      let validatorsArr = []
      dynamic.validators.forEach(item => {
        if (item.param) { // 如果有传参数,调用 Validators 方法
          validatorsArr.push(Validators[item.type](item.param))
        } else {
          validatorsArr.push(Validators[item.type])
        }
      })
      group[dynamic.key] = validatorsArr.length ? new FormControl(dynamic.value, validatorsArr) : new FormControl(dynamic.value);
    });
    return new FormGroup(group);
  }
}

上面会根据数据(比如:dynamic-form-data.service.ts)中的是否含有校验配置来动态生成响应式表单,并设置初始值。

// dynamic-form/shared/dynamic-form-data.service.ts
import { Injectable } from '@angular/core';
import { DynamicBase } from "./dynamic-base"
import { DynamicSelect } from "./dynamic-select"
import { DynamicText } from "./dynamic-text"
import { DynamicRadio } from "./dynamic-radio"

@Injectable()
export class DynamicFormDataService {
  constructor() { }
  getFormData() {
    let formItems: DynamicBase<any>[] = [
      new DynamicSelect({
        key: 'hobby',
        label: '爱好',
        value: 'basketball',
        options: [
          { key: 'basketball', value: '篮球' },
          { key: 'footer', value: '足球' },
          { key: 'tour', value: '旅游' },
          { key: 'run', value: '跑步' }
        ],
        order: 3
      }),
      new DynamicText({
        key: 'name',
        label: '用户名',
        value: 'Hello Word',
        validators: [
          {
            type: 'required',
            msg: '不能为空'
          }, {
            type: 'maxLength',
            param: 50,
            msg: '不能超过50个字符'
          }
        ],
        order: 1
      }),
      new DynamicText({
        key: 'email',
        label: '电子邮件',
        type: 'email',
        order: 2,
        validators: [
          {
            type: 'email',
            msg: 'email 格式不正确'
          }
        ],
      }),
      new DynamicRadio({
        key: 'sex',
        label: '性别',
        type: 'radio',
        order: 4,
        value: 0,
        items: [
          { label: "男", value: 0 },
          { label: "女", value: 1 }
        ]
      })
    ]
    // 按照 order 排序(升序)
    return formItems.sort((a, b) => a.order - b.order);
  }
}

上面这个 service 就是我们的表单数据,表单项就是根据这个来生成的。

不过上面从严格意义上来说,我们只是封装到了表单项层面,也就是说要想使用这个表单,我们就必需重复写 dynamic-from 组件的代码。而不可以直接调用 dynamic-from 组件。

如果想把 dynamic-form 作为动态表单对外调用 dynamic-form 组件,而不是 dynamic-form-item 组件,也说是作更高层次的抽象,我只需要在调用 dynamic-form 的组件时传数据或者同时额外传相关参数就可以了,这又怎么优化上面的代码呢?

如果想把 dynamic-form 作为动态表单对外调用 dynamic-form 组件,而不是 dynamic-form-item 组件,也说是作更高层次的抽象,我只需要在调用 dynamic-form 的组件时传数据或者同时额外传相关参数就可以了,这又怎么优化上面的代码呢?

此时我们可以对 dynamic-form 组件简单地修改就可以了,在 dynamic-form 组件中当点击保存按钮时向父组件发出一个事件并把响应式表单的数据传出让父组件来处理即可。

dynamic-form/dynamic-form.ts

import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicBase } from "./shared/dynamic-base"
import { DynamicFormGroupService } from './shared/dynamic-form-group.service';
@Component({
  selector: 'dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.less']
})
export class DynamicFormComponent implements OnInit {
  dynamicFormGroup: FormGroup
  @Input() formItems: DynamicBase<any>[]
  // 新添加的代码
  @Output() submitted: EventEmitter<any> = new EventEmitter<any>();
  // 注入 DynamicFormGroupService 服务
  constructor(private dynamicFormGroupService: DynamicFormGroupService) { }
  ngOnInit() {
    console.log(this.formItems);
    // 通过 DynamicFormGroupService 来创建一个 FormGroup
    this.dynamicFormGroup = this.dynamicFormGroupService.getFormGroup(this.formItems)
    console.log(this.dynamicFormGroup);
  }
  save() {
    // 改动过的代码
    const payLoad = JSON.stringify(this.dynamicFormGroup.value);
    // console.log(this.payLoad)
    this.submitted.emit(payLoad);
  }
}

现在我们就可以很简便地使用这个动态表单了。我们只需要在使用的地方通过标签调用就可以了。

比如在 app.component.html

<dynamic-form (submitted)="formSubmitted($event)" [formItems]="formItems"></dynamic-form>

这个组件只需要我们传入表单数据,以及添加一个来自子组件(dynamic-form)的事件监听就可以了。

接着在 app.component.ts 中添加事件处理方法

import { Component } from '@angular/core';
import { DynamicFormDataService } from './dynamic-form/shared/dynamic-form-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent {
  formItems: any[]
  constructor(private dynamicFormDataService: DynamicFormDataService) {
    this.formItems = dynamicFormDataService.getFormData();
    console.log(this.formItems);
  }
  // 新添加的事件处理方法
  formSubmitted (value: any) {
    console.log(value);
    // 这里添加你自己的逻辑
  }
}

这篇文章主要是分享动态表单的使用思路,具体可以根据自己的业务场景进行改造。比如:按钮的名字。