You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Angular动态表单中使用Signals?

基于Angular Signals重构搜索筛选组件的可行方案

原组件(类式输入输出版本)

TypeScript代码

import {
    Component,
    EventEmitter,
    Input,
    OnChanges,
    Output,
    SimpleChanges,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, Subscription } from 'rxjs';

export type SearchFilterOption = {
    kind: 'text' | 'search' | 'checkbox';
    label: string;
    key: string;
};

@Component({
    selector: 'shared-search-filter',
    standalone: true,
    imports: [CommonModule, ReactiveFormsModule],
    templateUrl: './search-filter.component.html',
})
export class SearchFilterComponent implements OnChanges {
    @Input() filter: { [key: string]: any } = {};
    @Input() filterOptions: readonly SearchFilterOption[] = [];
    @Output() filterChange = new EventEmitter<{ [key: string]: any }>();

    form = new FormGroup({});
    formChange?: Subscription;

    ngOnChanges(changes: SimpleChanges): void {
        if (changes['filterOptions']) {
            const group: { [key: string]: FormControl } = {};
            for (const filter of this.filterOptions) {
                group[filter.key] = new FormControl();
            }
            this.form = new FormGroup(group);
            this.formChange?.unsubscribe();
            this.formChange = this.form.valueChanges
                .pipe(
                    debounceTime(500),
                    distinctUntilChanged(
                        (a, b) => JSON.stringify(a) === JSON.stringify(b)
                    )
                )
                .subscribe({
                    next: (values) => {
                        this.filterChange.emit(values);
                    },
                });
        }
        if (changes['filter']) {
            for (const key of Object.keys(this.filter)) {
                const ctrl = this.form.get(key);
                if (ctrl) {
                    ctrl.setValue(this.filter[key]);
                }
            }
        }
    }
}

HTML模板

<fieldset [formGroup]="form">
    @for (option of filterOptions; track option; let i = $index) {
        <label>{{ option.label }}</label>
        <input [type]="option.kind" [formControlName]="option.key" />
    }
</fieldset>

问题描述

尝试改用Angular Signals重构输入输出后,出现表单字段缺失的问题,以下是错误的尝试代码:

错误尝试的TypeScript代码

filter = model<{ [key: string]: any }>({});
filterOptions = input.required<readonly SearchFilterOption[]>();

form = new FormGroup({});
formChange?: Subscription;

constructor() {
    effect(
        () => {
            const group: { [key: string]: FormControl } = {};
            for (const filter of this.filterOptions()) {
                group[filter.key] = new FormControl(
                    this.filter()[filter.key]
                );
            }
            this.form = new FormGroup(group);

            this.formChange?.unsubscribe();
            this.formChange = this.form.valueChanges
                .pipe(
                    debounceTime(500),
                    distinctUntilChanged(
                        (a, b) => JSON.stringify(a) === JSON.stringify(b)
                    )
                )
                .subscribe({
                    next: (values) => {
                        this.filter.set(values);
                    },
                });
        },
        { allowSignalWrites: true }
    );
}

错误尝试的HTML模板

<fieldset [formGroup]="form">
    @for (option of filterOptions(); track option; let i = $index) {
        <label>{{ option.label }}</label>
        <input [type]="option.kind" [formControlName]="option.key" />
    }
</fieldset>

可行的Signals实现方案

问题核心在于:原尝试中直接给普通变量form赋值,视图无法及时响应变更;且未单独处理filter更新到表单的逻辑,同时订阅管理不够严谨。以下是修复后的实现:

重构后的TypeScript代码

import {
    Component,
    effect,
    inject,
    input,
    model,
    DestroyRef,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, takeUntilDestroyed } from 'rxjs';

export type SearchFilterOption = {
    kind: 'text' | 'search' | 'checkbox';
    label: string;
    key: string;
};

@Component({
    selector: 'shared-search-filter',
    standalone: true,
    imports: [CommonModule, ReactiveFormsModule],
    templateUrl: './search-filter.component.html',
})
export class SearchFilterComponent {
    // 使用model实现双向绑定
    filter = model<{ [key: string]: any }>({});
    // 必填输入信号
    filterOptions = input.required<readonly SearchFilterOption[]>();
    // 用signal持有FormGroup,确保视图响应变化
    form = signal<FormGroup>(new FormGroup({}));
    // 用于自动销毁订阅
    private destroyRef = inject(DestroyRef);

    constructor() {
        // 1. 监听filterOptions变化,初始化/重建表单
        effect(() => {
            const options = this.filterOptions();
            const initialValues = this.filter();
            const controls: Record<string, FormControl> = {};

            for (const option of options) {
                controls[option.key] = new FormControl(initialValues[option.key] ?? null);
            }

            this.form.set(new FormGroup(controls));
        });

        // 2. 监听表单值变化,更新filter model
        effect(() => {
            const currentForm = this.form();
            currentForm.valueChanges
                .pipe(
                    debounceTime(500),
                    distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
                    // 自动销毁订阅,避免内存泄漏
                    takeUntilDestroyed(this.destroyRef)
                )
                .subscribe((values) => {
                    this.filter.set(values ?? {});
                });
        });

        // 3. 监听filter变化,同步到表单控件(避免循环更新)
        effect(() => {
            const currentFilter = this.filter();
            const currentForm = this.form();

            Object.entries(currentFilter).forEach(([key, value]) => {
                const control = currentForm.get(key);
                if (control && control.value !== value) {
                    // 设置emitEvent: false,防止触发valueChanges循环
                    control.setValue(value, { emitEvent: false });
                }
            });
        });
    }
}

重构后的HTML模板

<fieldset [formGroup]="form()">
    @for (option of filterOptions(); track option.key) {
        <label>{{ option.label }}</label>
        <input [type]="option.kind" [formControlName]="option.key" />
    }
</fieldset>

关键优化点

  • 用Signal持有FormGroup:确保表单实例变化时视图能及时更新,解决字段缺失问题
  • 拆分独立Effect:分别处理表单初始化、表单值同步到filter、filter同步到表单三个逻辑,职责更清晰
  • 自动订阅管理:使用takeUntilDestroyed替代手动取消订阅,避免内存泄漏
  • 避免循环更新:同步filter到表单时设置emitEvent: false,防止触发表单值变化的订阅循环
  • 优化Track表达式:用option.key作为track标识,比索引更可靠(key是唯一字段)

内容的提问来源于stack exchange,提问作者Manstie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 16:14:52