你已经掌握了响应式表单的「基本功」,能用 FormGroup 和 FormControl 搭建出结构固定的表单。但如果,你的表单不是「固定」的呢?
- 如果用户需要自己「添加多个」联系电话?
- 如果整个表单的结构,需要根据从服务器拉取的一份 JSON 配置来动态生成?
此时,你就需要学习响应式表单的「禁咒」级魔法 —— 动态表单。这是一种「无中生有」、「千变万化」的至高艺术,能让你从容应对任何复杂的表单需求。
魔法咒语:FormArray, addControl, removeControl#
要施展动态表单的魔法,你需要先认识几句关键「咒语」:
-
FormArray:这是你的「魔法口袋」,一个可以容纳动态数量控件的特殊数组。它是实现「动态增删」的基础。 -
addControl()/removeControl():在FormGroup上施放的「咒语」,允许你在运行时,向一个已存在的表单组中添加或移除一个控件。 -
push()/removeAt():在FormArray上施放的「咒语」,用于在表单数组的末尾添加一个新控件,或移除指定位置的控件。
场景一:「多重影分身之术」 —— 动态增删 FormArray#
这是动态表单最常见的应用场景。让我们再次请出「简历技能」这个经典例子。
组件代码:
// resume.component.ts
export class ResumeComponent {
private fb = inject(FormBuilder);
resumeForm = this.fb.group({
name: [''],
// 1. 初始化一个空的 FormArray
skills: this.fb.array([])
});
// 2. 为了方便在模板中使用,创建一个 getter
get skills() {
return this.resumeForm.get('skills') as FormArray;
}
// 3. 施法:添加一个新技能
addSkill() {
// push 一个新的 FormControl 到数组中
this.skills.push(this.fb.control('', Validators.required));
}
// 4. 施法:移除一个技能
removeSkill(index: number) {
this.skills.removeAt(index);
}
}模板代码:
<form [formGroup]="resumeForm">
<!-- ...其他字段... -->
<div formArrayName="skills">
<h3>技能列表</h3>
@for (skillControl of skills.controls; track i; let i = $index) {
<div>
<input [formControlName]="i" placeholder="请输入技能">
<button type="button" (click)="removeSkill(i)">-</button>
</div>
}
</div>
<button type="button" (click)="addSkill()">+ 添加技能</button>
</form>通过 FormArray,我们赋予了用户「创造」表单控件的能力,这正是动态表单魅力的初步体现。
场景二:「无中生有之术」 —— 根据配置生成表单#
这才是动态表单的「终极魔法」。想象一下,你的表单结构完全由后端 API 返回的一段 JSON 定义。
第一步:定义「魔法契约」(表单配置)
// 比如,从 API 获取到这样的配置数组
this.formConfig = [
{type: 'input', label: '姓名', name: 'name', validation: {required: true}},
{type: 'select', label: '国家', name: 'country', options: ['中国', '美国', '日本']},
{type: 'checkbox', label: '同意条款', name: 'terms', validation: {required: true}}
];
interface FormField {
type: 'input' | 'select' | 'checkbox';
label: string;
name: string;
options?: string[]; // For select type
validation?: {
required?: boolean;
// ... other validation rules
};
}第二步:在组件中「吟唱咒语」(动态构建表单)
// survey.component.ts
export class SurveyComponent implements OnInit {
private fb = inject(FormBuilder);
formConfig: FormField[] = []; // 将从 API 获取
form: FormGroup;
ngOnInit() {
// 假设在这里通过 API 获取了 this.formConfig
this.form = this.buildFormFromConfig(this.formConfig);
}
private buildFormFromConfig(config: FormField[]): FormGroup {
const group = this.fb.group({});
config.forEach(field => {
// 根据配置,动态添加一个又一个 FormControl
const control = this.fb.control('', this.mapValidators(field.validation));
group.addControl(field.name, control);
});
return group;
}
// 一个辅助函数,将 JSON 配置映射成真正的校验器
private mapValidators(validators: any) {
const formValidators = [];
if (validators) {
if (validators.required) {
formValidators.push(Validators.required);
}
// ... 其他校验器
}
return formValidators;
}
}第三步:在模板中「召唤万物」(动态渲染视图)
模板不再是写死的,而是变成了一个能「解析」配置并「召唤」出对应控件的「魔法阵」。
<form [formGroup]="form">
@for (field of formConfig; track field.name) {
<div class="form-field">
<label>{{ field.label }}</label>
<div [ngSwitch]="field.type">
@case ('input') {
<input type="text" [formControlName]="field.name">
}
@case ('select') {
<select [formControlName]="field.name">
@for (opt of field.options; track opt) {
<option [value]="opt">{{ opt }}</option>
}
</select>
}
@case ('checkbox') {
<input type="checkbox" [formControlName]="field.name">
}
</div>
<!-- 还可以动态渲染错误信息 -->
</div>
}
</form>ngSwitch (或 @switch) 在这里是关键。它像一个「分类帽」,根据配置中的 type,决定为当前的 FormControl 召唤出哪一种具体的 HTML 输入元素。
结语#
动态表单,是响应式表单强大控制力的集中体现。它实现了视图、模型、配置三者的完美分离。
-
配置 (JSON) 定义了表单的「灵魂」 —— 「有什么,怎么验」。
-
组件类 (TS) 根据配置,构建出表单的「骨架」 ——
FormGroup。 -
模板 (HTML) 则负责将这个「骨架」渲染成用户可见的「血肉」。
当你掌握了这套「无中生有」的魔法,你就不再是一个亦步亦趋的「表单使用者」,而升级为了一位能够定义规则、创造引擎的「表单大法师」。任何复杂、善变的表单需求,在你面前,都将化为指尖跳动的「咒语」而已。
正如《道德经》所言:「道生一,一生二,二生三,三生万物。」意指万事万物的生成,皆源于简单而根本的规律。动态表单正是这种「道」的体现,它从简单的配置中演化出千变万化的表单结构,让我们能以不变应万变,从容驾驭复杂多变的前端世界。