如何让信号表单具备动态结构?

前端开发 2026-07-09

为了让我的应用中的表单管理更简单,我实现了一个表单组件:

export type MyFormModel = Record<string, number|string>;

export type MyFormFields = MyFormField[];
export type MyFormField = MyFormFieldRange | MyFormFieldText;
export interface MyFormFieldRange {
  type: "range";
  key: string;
  min: number;
  max: number;
}
export interface MyFormFieldText {
  type: "text";
  key: string;
  required?: boolean;
}

export class MyForm {
  fields = input.required<MyFormFields>();
  model = model<MyFormModel>({});

  form = form(this.model);
}

然而我还没有找到基于所提供的fields来生成模式的方法。

我尝试对form使用计算信号。但是,在调用表单函数时,我收到了如下错误信息:

_effect-chunk2.mjs:2601 ERROR RuntimeError: NG0203: The `_Injector` token injection failed. `inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`.

也尝试在ngOnInitngOnChanges中延迟form的初始化,但仍然得到相同的错误。

最后尝试在模式函数中消费fields,但结果是:

ERROR RuntimeError: NG0950: Input "fields" is required but no value is available yet. Find more at https://v21.angular.dev/errors/NG0950

解决方案

据我所知,目前没有任何API能让模式像这样实现动态。

有一个相关的正在进行中的 问题,描述的是有人试图通过把字段配置放入模型来创建动态表单。正如那里所说,你可以使用 validateStandardSchema 让它动态,但这相当有限制。

然而,你可以通过在 fields 改变时重新创建表单来实现。为此,将表单和模式的创建移到 ngOnChanges。在那里,你需要创建你自己的 Injector,并且每次 fields 改变时都将其销毁,因为 form 正在注册一个需要在重新创建时清理的 effect

export class MyForm {
  injector = inject(Injector);
  fields = input.required<MyFormFields>();
  fieldsByKey = computed(() => new Map(this.fields().map((x) => [x.key, x])));
  model = model.required<MyFormModel>();
  form!: FieldTree<MyFormModel, string | number>;
  formInjector: DestroyableInjector | undefined;

  schemaByType = {
    range: (
      key: string,
      schema: SchemaPathTree<MyFormModel, PathKind.Root>
    ) => {
      min(schema[key], () => this.getField<MyFormFieldRange>(key).min);
      max(schema[key], () => this.getField<MyFormFieldRange>(key).max);
    },
    text: (key: string, schema: SchemaPathTree<MyFormModel, PathKind.Root>) => {
      required(schema[key], {
        when: () => this.getField<MyFormFieldText>(key).required ?? false,
      });
    },
  };

  private getField<T>(key: string): T {
    return this.fieldsByKey().get(key) as T;
  }

  public ngOnDestroy() {
    this.formInjector?.destroy();
  }

  public ngOnChanges({ fields }: { fields?: SimpleChange<MyFormFields> }) {
    if (fields !== undefined) {
      if (this.formInjector != undefined) this.formInjector.destroy();
      this.formInjector = Injector.create({
        providers: [],
        parent: this.injector,
      });
      this.form = form(
        this.model,
        (schema) => {
          fields.currentValue.forEach((f) => {
            this.schemaByType[f.type](f.key, schema);
          });
        },
        { injector: this.formInjector }
      );
    }
  }
}

完整示例

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章