背景
开发过程中一般情况下表单项确定的时候表单是写死的,但是还有一些情况相同的页面需要根据服务端返回的不同业务类型,展示不同的表单项。
问题:
当业务类型较少时,可以在组件中写死几个表单根据不同类型展示不同的表单,或者新增表单组件,展示不同的表单组件。但是当后续需要新增业务类型,这时候又需要修改客户端代码,增加新的业务表单,表单代码越来越多不利于维护。
解决办法:
考虑一种生成动态表单办法,后续如果新增业务表单,表单组件也不需要修改,只需要在服务端增加配置,不同的业务返回不同的表单,前端根据服务端返回的配置解析并展示对应表单项。
示例:
1、基本实现
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FormService {
constructor(private http: HttpClient) {}
getFormConfig(): Observable<any> {
return of({
fields: [
{
name: 'selection',
type: 'select',
options: [
{ value: 'A', label: 'Option A' },
{ value: 'B', label: 'Option B' }
]
},
{
name: 'email',
type: 'text',
condition: { field: 'selection', value: 'A' }
},
{
name: 'phone',
type: 'text',
condition: { field: 'selection', value: 'B' }
}
]
}).pipe(delay(500)); // 模拟网络延迟
}
}
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
import { FormService } from './form.service';
@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit {
formGroup: FormGroup;
formConfig: any;
constructor(private fb: FormBuilder, private formService: FormService) {}
ngOnInit() {
this.loadFormConfig();
}
loadFormConfig() {
this.formService.getFormConfig().subscribe(config => {
this.formConfig = config;
this.initializeForm();
this.setupConditionalFields();
});
}
initializeForm() {
const formControls = {};
this.formConfig.fields.forEach(field => {
formControls[field.name] = new FormControl('');
});
this.formGroup = this.fb.group(formControls);
}
setupConditionalFields() {
this.formConfig.fields.forEach(field => {
if (field.condition) {
this.formGroup.get(field.condition.field)?.valueChanges.subscribe(value => {
if (value === field.condition.value) {
this.formGroup.addControl(field.name, new FormControl(''));
} else {
this.formGroup.removeControl(field.name);
}
});
}
});
}
onSubmit() {
if (this.formGroup.valid) {
console.log('Form submitted:', this.formGroup.value);
} else {
console.log('Form is invalid');
}
}
}
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<div *ngFor="let field of formConfig.fields">
<div *ngIf="!field.condition || (field.condition && formGroup.contains(field.name))">
<label [for]="field.name">{{ field.name }}:</label>
<ng-container [ngSwitch]="field.type">
<select *ngSwitchCase="'select'" [id]="field.name" [formControlName]="field.name">
<option *ngFor="let option of field.options" [value]="option.value">{{ option.label }}</option>
</select>
<input *ngSwitchCase="'text'" [id]="field.name" [formControlName]="field.name" type="text">
</ng-container>
</div>
</div>
<button type="submit" [disabled]="formGroup.invalid">Submit</button>
</form>
2、可移动表单项(使用FormArray 管理表单项,并提供方法来调整表单项顺序)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FormService {
constructor(private http: HttpClient) {}
getFormConfig(): Observable<any> {
return of({
fields: [
{
name: 'selection',
type: 'select',
options: [
{ value: 'A', label: 'Option A' },
{ value: 'B', label: 'Option B' }
]
},
{
name: 'email',
type: 'text',
condition: { field: 'selection', value: 'A' }
},
{
name: 'phone',
type: 'text',
condition: { field: 'selection', value: 'B' }
}
]
}).pipe(delay(500)); // 模拟网络延迟
}
}
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, FormControl } from '@angular/forms';
import { FormService } from './form.service';
@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit {
formGroup: FormGroup;
formConfig: any;
constructor(private fb: FormBuilder, private formService: FormService) {}
ngOnInit() {
this.loadFormConfig();
}
loadFormConfig() {
this.formService.getFormConfig().subscribe(config => {
this.formConfig = config;
this.initializeForm();
this.setupConditionalFields();
});
}
initializeForm() {
this.formGroup = this.fb.group({
fields: this.fb.array([])
});
this.formConfig.fields.forEach(field => {
this.addField(field);
});
}
addField(field: any) {
const formArray = this.formGroup.get('fields') as FormArray;
formArray.push(this.createFormGroupForField(field));
}
createFormGroupForField(field: any) {
return this.fb.group({
name: [field.name],
type: [field.type],
value: [''],
condition: [field.condition]
});
}
setupConditionalFields() {
this.formConfig.fields.forEach(field => {
if (field.condition) {
this.formGroup.get(field.condition.field)?.valueChanges.subscribe(value => {
if (value === field.condition.value) {
this.addField(field);
} else {
this.removeField(field.name);
}
});
}
});
}
removeField(fieldName: string) {
const formArray = this.formGroup.get('fields') as FormArray;
const index = formArray.controls.findIndex(control => control.get('name').value === fieldName);
if (index !== -1) {
formArray.removeAt(index);
}
}
moveFieldUp(fieldName: string) {
const formArray = this.formGroup.get('fields') as FormArray;
const index = formArray.controls.findIndex(control => control.get('name').value === fieldName);
if (index > 0) {
const temp = formArray.at(index);
formArray.setControl(index, formArray.at(index - 1));
formArray.setControl(index - 1, temp);
}
}
moveFieldDown(fieldName: string) {
const formArray = this.formGroup.get('fields') as FormArray;
const index = formArray.controls.findIndex(control => control.get('name').value === fieldName);
if (index < formArray.length - 1) {
const temp = formArray.at(index);
formArray.setControl(index, formArray.at(index + 1));
formArray.setControl(index + 1, temp);
}
}
onSubmit() {
if (this.formGroup.valid) {
console.log('Form submitted:', this.formGroup.value);
} else {
console.log('Form is invalid');
}
}
}
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<div formArrayName="fields">
<div *ngFor="let field of formGroup.get('fields').controls; let i = index" [formGroupName]="i">
<div *ngIf="!field.get('condition').value || (field.get('condition').value && formGroup.contains(field.get('condition').value.field))">
<label [for]="field.get('name').value">{{ field.get('name').value }}:</label>
<ng-container [ngSwitch]="field.get('type').value">
<select *ngSwitchCase="'select'" [id]="field.get('name').value" formControlName="value">
<option *ngFor="let option of formConfig.fields[i].options" [value]="option.value">{{ option.label }}</option>
</select>
<input *ngSwitchCase="'text'" [id]="field.get('name').value" formControlName="value" type="text">
</ng-container>
<button (click)="moveFieldUp(field.get('name').value)">Move Up</button>
<button (click)="moveFieldDown(field.get('name').value)">Move Down</button>
</div>
</div>
</div>
<button type="submit" [disabled]="formGroup.invalid">Submit</button>
</form>
关键点说明
-
使用
FormArray管理表单项:initializeForm方法初始化FormArray,并将表单项添加到FormArray中。addField方法将表单项添加到FormArray中。createFormGroupForField方法创建一个FormGroup对象,表示一个表单项。
-
调整
FormArray中的controls数组:moveFieldUp和moveFieldDown方法通过交换controls数组中的元素位置来实现表单项的上下移动。
-
模板:
- 使用
*ngFor指令遍历FormArray中的表单项。 - 使用
*ngIf指令根据条件决定是否显示表单项。 - 使用
ngSwitch指令根据字段类型生成不同的输入元素。 - 添加按钮来调整表单项的顺序,调用
moveFieldUp和moveFieldDown方法
- 使用
3、添加校验信息
配置文件
const formConfig = {
fields: [
{
name: 'category',
type: 'select',
label: '类别',
options: [
{ value: 'option1', label: '选项1' },
{ value: 'option2', label: '选项2' }
],
validators: ['required']
},
{
name: 'subField1',
type: 'text',
label: '子字段1',
visible: 'category === "option1"',
dependsOn: ['category'],
validators: ['required', 'minLength:5', 'pattern:/^[a-zA-Z0-9]+$/']
},
{
name: 'subField2',
type: 'text',
label: '子字段2',
visible: 'category === "option2"',
dependsOn: ['category'],
validators: ['required', 'maxLength:10', 'pattern:/^[a-zA-Z]+$/']
}
]
};
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-dynamic-form',
template: `
<form [formGroup]="form">
<div *ngFor="let field of formConfig.fields">
<ng-container *ngIf="isFieldVisible(field)">
<label [for]="field.name">{{ field.label }}</label>
<ng-container [ngSwitch]="field.type">
<select *ngSwitchCase="'select'" [formControlName]="field.name">
<option *ngFor="let option of field.options" [value]="option.value">{{ option.label }}</option>
</select>
<input *ngSwitchCase="'text'" [formControlName]="field.name" type="text" />
</ng-container>
<div *ngIf="form.get(field.name)?.invalid && form.get(field.name)?.touched" class="error">
<div *ngIf="form.get(field.name)?.errors?.['required']">此字段是必填的。</div>
<div *ngIf="form.get(field.name)?.errors?.['minlength']">最小长度为 {{ form.get(field.name)?.errors?.['minlength'].requiredLength }}。</div>
<div *ngIf="form.get(field.name)?.errors?.['maxlength']">最大长度为 {{ form.get(field.name)?.errors?.['maxlength'].requiredLength }}。</div>
<div *ngIf="form.get(field.name)?.errors?.['pattern']">格式不正确。</div>
</div>
</ng-container>
</div>
<button (click)="onSubmit()">提交</button>
</form>
`,
styles: [`
.error { color: red; }
`]
})
export class DynamicFormComponent implements OnInit {
form: FormGroup;
formConfig = formConfig;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group(
this.formConfig.fields.reduce((acc, field) => {
acc[field.name] = this.createFormControl(field);
return acc;
}, {})
);
// 监听表单值的变化,更新可见性
this.form.valueChanges.subscribe(() => {
this.updateVisibility();
});
}
createFormControl(field: any): FormControl {
const validators = field.validators?.map(validator => {
if (validator.startsWith('required')) {
return Validators.required;
} else if (validator.startsWith('minLength:')) {
const length = parseInt(validator.split(':')[1], 10);
return Validators.minLength(length);
} else if (validator.startsWith('maxLength:')) {
const length = parseInt(validator.split(':')[1], 10);
return Validators.maxLength(length);
} else if (validator.startsWith('pattern:')) {
const pattern = validator.split(':')[1];
return Validators.pattern(pattern);
}
return null;
}).filter(Boolean);
return new FormControl('', validators);
}
isFieldVisible(field: any): boolean {
if (!field.visible) {
return true;
}
const expression = field.visible;
const values = this.form.value;
return new Function('values', `return ${expression};`)(values);
}
updateVisibility() {
this.formConfig.fields.forEach(field => {
if (field.dependsOn) {
const control = this.form.get(field.name);
if (this.isFieldVisible(field)) {
control.enable();
} else {
control.disable();
}
}
});
}
onSubmit() {
if (this.form.valid) {
console.log('表单提交:', this.form.value);
} else {
this.form.markAllAsTouched();
}
}
}
解释
-
配置文件:
validators属性用于定义每个字段的校验规则,支持内置校验器(如required、minLength、maxLength)和自定义正则表达式校验(如pattern:/^[a-zA-Z0-9]+$/)。
-
动态表单组件:
createFormControl方法根据配置文件中的validators属性创建FormControl,并应用相应的校验器。isFieldVisible方法使用new Function动态解析visible表达式,判断字段是否可见。updateVisibility方法在表单值变化时调用isFieldVisible方法,根据依赖字段的值更新字段的可见性和启用状态。onSubmit方法在表单提交时检查表单的有效性,并在无效时标记所有字段为已触摸状态。
校验规则解析
- required:必填校验。
- minLength:5:最小长度为 5。
- maxLength:10:最大长度为 10。
- pattern:/^[a-zA-Z0-9]+$/ :正则表达式校验,确保输入只包含字母和数字。
通过这种方式,你可以在配置文件中定义字段的校验规则,包括自定义的正则表达式校验,并在动态表单组件中处理这些校验规则。
自定义校验器器
export function myCustomValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
// 校验逻辑
if (value && value.length < 5) {
return { 'tooShort': true }; // 返回错误信息
}
return null; // 校验成功
};
}
使用示例:
组件
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { myCustomValidator } from './path-to-validator';
@Component({
selector: 'app-my-form',
templateUrl: './my-form.component.html'
})
export class MyFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
username: ['', [Validators.required, myCustomValidator()]]
});
}
}
模板
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label for="username">Username:</label>
<input id="username" formControlName="username" type="text">
<div *ngIf="form.controls.username.touched && form.controls.username.invalid">
<small *ngIf="form.controls.username.errors?.required">Username is required.</small>
<small *ngIf="form.controls.username.errors?.tooShort">Username must be at least 5 characters long.</small>
</div>
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>