你已经掌握了响应式表单的「基本功」,能用 FormGroupFormControl 搭建出结构固定的表单。但如果,你的表单不是「固定」的呢?

  • 如果用户需要自己「添加多个」联系电话?
  • 如果整个表单的结构,需要根据从服务器拉取的一份 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) 则负责将这个「骨架」渲染成用户可见的「血肉」。

当你掌握了这套「无中生有」的魔法,你就不再是一个亦步亦趋的「表单使用者」,而升级为了一位能够定义规则、创造引擎的「表单大法师」。任何复杂、善变的表单需求,在你面前,都将化为指尖跳动的「咒语」而已。

正如《道德经》所言:「道生一,一生二,二生三,三生万物。」意指万事万物的生成,皆源于简单而根本的规律。动态表单正是这种「道」的体现,它从简单的配置中演化出千变万化的表单结构,让我们能以不变应万变,从容驾驭复杂多变的前端世界。