首页
/ Angular Signal Forms 自定义控件开发指南:控制接口、状态绑定与值转换的完整实现

Angular Signal Forms 自定义控件开发指南:控制接口、状态绑定与值转换的完整实现

2026-09-07 14:15:11作者:明树来

导读:浏览器自带的 inputselecttextarea 只能覆盖常见输入场景,日期选择器、富文本编辑器、自动补全标签选择器等特殊控件必须由应用自行实现。本文基于 Angular 仓库中 Signal Forms(信号表单)的自定义控件官方指南,讲解如何通过 FormValueControl / FormCheckboxControl 控制接口与 [formField] 指令,让任何组件无缝接入表单的值绑定、校验与状态管理,并深入剖析其源码实现。读完本文,你将能够实现最小可用控件、接入 touched/disabled/errors 等状态信号、处理 UI 值与模型值的转换(parse/format)与解析错误,并打造可跨表单复用的控件配套 schema。

前置知识:Signal Forms 的控件接入模型

本文假设你已经熟悉 Signal Forms 基础概念。Signal Forms 与「模板驱动表单 / 响应式表单」有一个本质区别:接入表单系统的不是一个装饰器或访问器注册表,而是一组纯 TypeScript 接口契约(control interface)

只要组件实现了特定的控制接口,并用 [formField] 指令把它绑定到一个字段(FieldTree)上,该指令就会自动完成三件事:双向值绑定、表单状态(disabled、errors、touched 等)同步、以及校验约束值透传。对 Signal Forms 而言,内置控件与自定义控件是「同一类公民」——指令不区分二者,校验、状态更新与数据绑定对两者完全一致。

一个非常实用的兼容性结论(在原文档中有明确说明):Signal Forms 自定义控件可以在不做任何额外兼容代码的前提下,与 Signal、Reactive(响应式)和 Template-Driven(模板驱动)三种表单混用,详见 Migration 指南 中关于自定义控件的章节。

创建最基础的自定义控件

先从一个尽量精简的实现入手,逐步添加能力。

最小文本输入控件:FormValueControl

文本、数字、日期选择、下拉选择等「编辑单个值」的控件,只需要实现 FormValueControl 接口并提供一个必需的 value model 信号:

import {Component, model} from '@angular/core';
import {FormValueControl} from '@angular/forms/signals';

@Component({
  selector: 'app-basic-input',
  template: `
    <div class="basic-input">
      <input
        type="text"
        [value]="value()"
        (input)="value.set($event.target.value)"
        placeholder="Enter text..."
      />
    </div>
  `,
})
export class BasicInput implements FormValueControl<string> {
  /** The current input value */
  value = model('');
}

实现要点:

  • 模板中用 [value]="value()" 把内部 <input> 渲染值绑定到组件的 value model 信号上;
  • 原生 (input) 事件触发时调用 value.set(...),把用户输入写回 model 信号;
  • value = model('') 是 Angular 的 model 信号(双向绑定原语),外部通过 [formField] 与它同步,双方任一方的写入都会通过信号响应自动传播到另一方。

最小开关/复选框控件:FormCheckboxControl

「布尔开/关」类的控件(开关、切换按钮、复选框)需要:

  1. 实现 FormCheckboxControl 接口,让 [formField] 指令能识别它为表单控件;
  2. 提供一个 checked model 信号。
import {Component, model, ChangeDetectionStrategy} from '@angular/core';
import {FormCheckboxControl} from '@angular/forms/signals';

@Component({
  selector: 'app-basic-toggle',
  template: `
    <button type="button" [class.active]="checked()" (click)="toggle()">
      <span class="toggle-slider"></span>
    </button>
  `,
})
export class BasicToggle implements FormCheckboxControl {
  /** Whether the toggle is checked */
  checked = model<boolean>(false);

  toggle() {
    this.checked.update((val) => !val);
  }
}

FormValueControl 的差异只在于「用哪个信号承载值」:布尔控件用 checked,普通输入控件用 value

在表单中使用自定义控件

控件写好后,凡是内置输入框能出现的位置,它都能出现——只需为它加上 [formField] 指令:

import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
import {BasicInput} from './basic-input';
import {BasicToggle} from './basic-toggle';

@Component({
  imports: [FormField, BasicInput, BasicToggle],
  template: `
    <form novalidate>
      <label>
        Email
        <app-basic-input [formField]="registrationForm.email" />
      </label>

      <label>
        Accept terms
        <app-basic-toggle [formField]="registrationForm.acceptTerms" />
      </label>

      <button type="submit" [disabled]="registrationForm().invalid()">Register</button>
    </form>
  `,
})
export class Registration {
  registrationModel = signal({
    email: '',
    acceptTerms: false,
  });

  registrationForm = form(this.registrationModel, (schemaPath) => {
    required(schemaPath.email, {message: 'Email is required'});
    required(schemaPath.acceptTerms, {message: 'You must accept the terms'});
  });
}

值得注意:form() 的 schema 回调参数(此处命名为 schemaPath)是一个 SchemaPathTree 对象,为表单中所有字段提供访问路径,参数名可以任意起。示例中的按钮禁用逻辑 registrationForm().invalid() 由表单整体校验状态驱动——自定义控件与内置输入在其中的地位完全等价。

深入理解控制接口契约

前两个组件之所以能被识别,是因为它们实现了不同的控制接口,这些接口充当「Signal Forms 与控件通信的协议」。

FormValueControl:单值输入控件

适用于文本输入、数字输入、日期选择、下拉选择等一切「编辑单个值」的控件。实现该接口:

  • 必需属性:提供一个 value model 信号;
  • [formField] 指令行为:把表单字段的值绑定到该 value 信号上。

IMPORTANT:实现 FormValueControl 的控件不得再定义 checked 属性。

FormCheckboxControl:布尔开关类控件

适用于开关、切换、复选框等「表示布尔开/关状态」的控件。实现该接口:

  • 必需属性:提供一个 checked model 信号;
  • [formField] 指令行为:把表单字段的值绑定到该 checked 信号上。

IMPORTANT:实现 FormCheckboxControl 的控件不得再定义 value 属性。

从源码层面看,这条「互斥」约束被 TypeScript 严格编码在接口里:FormValueControl 声明 readonly checked?: undefinedFormCheckboxControl 声明 readonly value?: undefined,见 api/control.ts。源码注释还透露,之所以把 value/checked 互斥写死,是因为未来可能引入第三种「同时定义 valuechecked」的单选按钮(radio)控件契约,目前仍在评估中——这说明接口设计为演进留了余地。

基接口 FormUiControl 与可选状态属性

FormValueControlFormCheckboxControl 都继承自 FormUiControl(源码见 api/control.ts),后者提供了与表单状态集成的一系列可选属性。所有属性均为可选,只实现你的控件真正需要的即可。

FormUiControl 中的属性类型均为 InputSignal(允许 InputSignalWithTransform),意味着指令会把对应状态「推送」给你的输入信号。可选项按用途分为四类:

交互状态(interaction state):追踪用户是否操作过控件

Property Purpose
touched 用户是否与该字段发生过交互
dirty 当前值是否与初始值不同
touch(输出) 输出事件,标记字段已被触及(详见下文 debounce('blur') 一节)

校验状态(validation state):展示校验反馈

Property Purpose
errors 当前校验错误数组
valid 字段是否有效
invalid 字段是否存在校验错误
pending 异步校验是否进行中

可用性状态(availability state):控制用户能否操作字段

Property Purpose
disabled 字段是否被禁用
disabledReasons 字段被禁用的原因列表
readonly 字段是否只读(可见但不可编辑)
hidden 字段是否从视图中隐藏

disabledReasons 是一个 DisabledReason 对象数组。每个对象包含一个指向字段树的 field 引用属性,以及可选的 message 属性;读取提示文案使用 reason.message

校验约束(validation constraints):从表单接收约束值

Property Purpose
required 字段是否为必填
min 最小数值(无约束时为 undefined
max 最大数值(无约束时为 undefined
minLength 最小字符串长度(无约束时为 undefined
maxLength 最大字符串长度(无约束时为 undefined
pattern 需要匹配的正则表达式数组

值得注意的源码细节:在 api/control.ts 中,min/max 的真实类型是 NonNullable<TValue> | undefinedpatternreadonly RegExp[]——即这些输入的类型跟随表单字段的泛型类型。文档表格以「数值」为例便于理解,实际实现是泛型的。

字段元数据(field metadata)

Property Purpose
name 字段的 name 属性(在表单与应用范围内唯一)

此外,FormUiControl 还声明了 focus?(聚焦 UI 控件;若未实现,Signal Forms 会尝试聚焦宿主元素)与 reset?(将 UI 控件重置为初始状态)两个可选方法。下一节「添加状态信号」展示如何在控件中落地这些属性。

[formField] 指令的工作机制

[formField] 指令会检测控件实现的是哪个接口,并自动绑定对应信号。指令本身是一个 [formField] 属性选择器,源码见 directive/form_field.ts。当把 [formField] 用在自定义组件上时,接入过程可归纳为:

import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required} from '@angular/forms/signals';
import {CustomInput} from './custom-input';
import {CustomToggle} from './custom-toggle';

@Component({
  selector: 'app-my-form',
  imports: [FormField, CustomInput, CustomToggle],
  template: `
    <form novalidate>
      <app-custom-input [formField]="userForm.username" />
      <app-custom-toggle [formField]="userForm.subscribe" />
    </form>
  `,
})
export class MyForm {
  formModel = signal({
    username: '',
    subscribe: false,
  });

  userForm = form(this.formModel, (schemaPath) => {
    required(schemaPath.username, {message: 'Username is required'});
  });
}

当绑定 [formField]="userForm.username" 时,指令依次执行:

  1. 检测控件实现了 FormValueControl(还是 FormCheckboxControl);
  2. 内部读取 userForm.username().value() 并绑定到控件的 value model 信号;
  3. 把表单状态信号(disabled()errors() 等)绑定到控件已实现的可选输入信号上;
  4. 之后所有更新都经由信号响应机制自动完成。

从底层源码看,检测与装配发生在 FormField.ɵngControlCreate 生命周期钩子中(form_field.ts),其优先级依次为:宿主上有 ControlValueAccessor(为兼容响应式表单)→ 宿主是自定义控件(host.customControl)→ 宿主是原生表单元素 → 否则抛出 INVALID_FIELD_DIRECTIVE_HOST 运行时错误,提示宿主必须是原生控件或带有 value/checked model 的自定义控件。这也印证了文档的结论:自定义控件与内置输入的 [formField] 用法完全一致

负责装配自定义控件的是 customControlCreatedirective/control_custom.ts),它:

  • 通过 listenToCustomControlModel 监听控件的 model 变化并写入字段状态(state.controlValue.set(value));
  • 通过 listenToCustomControlOutput('touch', ...) 监听 touch 输出,回调 state.markAsTouched()
  • 调用 registerAsBinding 把自身注册为字段的绑定;
  • 在每个变更检测轮次把 controlValue 以及 CONTROL_BINDING_NAMES 列出的全部状态绑定回控件输入信号。

关于 form model 的完整创建与管理,见 Form Models 指南

添加状态信号,让控件响应表单状态

前面最简控件「能用但不能感知状态」。通过添加可选输入信号,可以让控件响应禁用状态、渲染校验错误并记录交互痕迹。下面是一个实现了常用状态属性的完整示例:

import {Component, model, input, output, ChangeDetectionStrategy} from '@angular/core';
import {
  FormValueControl,
  WithOptionalFieldTree,
  ValidationError,
  DisabledReason,
} from '@angular/forms/signals';

@Component({
  selector: 'app-stateful-input',
  template: `
    @if (!hidden()) {
      <div class="input-container">
        <input
          type="text"
          [value]="value()"
          (input)="value.set($event.target.value)"
          [disabled]="disabled()"
          [readonly]="readonly()"
          [class.invalid]="invalid()"
          [attr.aria-invalid]="invalid()"
          (blur)="touch.emit()"
        />

        @if (invalid()) {
          <div class="error-messages" role="alert">
            @for (error of errors(); track error) {
              <span class="error">{{ error.message }}</span>
            }
          </div>
        }

        @if (disabled() && disabledReasons().length > 0) {
          <div class="disabled-reasons">
            @for (reason of disabledReasons(); track reason) {
              <span>{{ reason.message }}</span>
            }
          </div>
        }
      </div>
    }
  `,
})
export class StatefulInput implements FormValueControl<string> {
  // Required
  value = model<string>('');

  // Writable interaction state - control updates these
  touched = input<boolean>(false);
  touch = output<void>();

  // Read-only state - form system manages these
  disabled = input<boolean>(false);
  disabledReasons = input<readonly DisabledReason[]>([]);
  readonly = input<boolean>(false);
  hidden = input<boolean>(false);
  invalid = input<boolean>(false);
  errors = input<readonly WithOptionalFieldTree<ValidationError>[]>([]);
}

该示例展示了一条重要分工原则:

  • 控件自身维护value(用户输入)以及 touched/touch(用户交互);模板中 (blur)="touch.emit()" 把原生失焦事件上报给表单系统;
  • 表单系统维护并推送disableddisabledReasonsreadonlyhiddeninvaliderrors——控件只负责声明 input(...),指令在字段状态变化时自动填充。

WithOptionalFieldTree<ValidationError> 的含义是「错误对象可携带一个可选的 fieldTree 引用」(详见 validation_errors.ts),便于追踪错误来源字段。ValidationError 类型还包含 kindmessage 等字段。

随后就能结合校验与状态管理使用该控件:

import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {form, FormField, required, email} from '@angular/forms/signals';
import {StatefulInput} from './stateful-input';

@Component({
  imports: [FormField, StatefulInput],
  template: `
    <form novalidate>
      <label>
        Email
        <app-stateful-input [formField]="loginForm.email" />
      </label>
    </form>
  `,
})
export class Login {
  loginModel = signal({email: ''});

  loginForm = form(this.loginModel, (schemaPath) => {
    required(schemaPath.email, {message: 'Email is required'});
    email(schemaPath.email, {message: 'Enter a valid email address'});
  });
}

用户输入非法邮箱时,[formField] 指令会自动更新 invalid()errors() 信号,控件随之渲染错误提示——自定义控件无需自行实现任何校验判定逻辑。

debounce('blur') 协同:touch 输出约定

debounce('blur') 规则会把 UI 到表单模型的更新延迟到字段失焦时才提交,而非每次按键都更新。内置控件会自动向表单报告 blur;自定义控件只有在响应原生 blur 事件时发出 touch 输出,才会参与该机制

import {Component, model, output} from '@angular/core';
import {FormValueControl} from '@angular/forms/signals';

@Component({
  selector: 'app-custom-input',
  template: `
    <input
      type="text"
      [value]="value()"
      (input)="value.set($event.target.value)"
      (blur)="touch.emit()"
    />
  `,
})
export class CustomInput implements FormValueControl<string> {
  value = model('');
  touch = output<void>();
}

一旦具备 touch 输出,debounce('blur') 对自定义控件的行为将与内置输入完全一致:

import {Component, signal} from '@angular/core';
import {debounce, form, FormField} from '@angular/forms/signals';
import {CustomInput} from './custom-input';

@Component({
  selector: 'app-root',
  imports: [CustomInput, FormField],
  template: `<app-custom-input [formField]="userForm.name" />`,
})
export class App {
  userModel = signal({name: ''});

  userForm = form(this.userModel, (schemaPath) => {
    debounce(schemaPath.name, 'blur');
  });
}

IMPORTANT:touch 必须在 blur(焦点离开控件)时发出,而不是在 focus 时。没有 touch 输出,字段永远不会被标记为失焦,debounce('blur') 对你的控件将完全不生效。

对应的底层连接位于 directive/control_custom.ts:指令通过 listenToCustomControlOutput('touch', () => parent.state().markAsTouched())touch 输出接到字段的 markAsTouched() 上,这也是后续 blur 类规则能够依赖 touched 状态的原因。

值转换:UI 显示值 ≠ 模型存储值

有些控件在 UI 上展示的值与表单模型存储的值并不一致——日期选择器可能显示「January 15, 2024」而模型存 2024-01-15;货币输入框可能显示 $1,234.56 而模型存 1234.56

此时使用 transformedValue()(同样导出自 @angular/forms/signals)来让 UI 的「原始值(raw value)」与模型值保持同步。它接收控件的 value model 信号外加一对 parse / format 函数,返回一个承载原始值的可写信号

  • format:把模型值转换为模板渲染所需的原始值;
  • parse:把用户输入解析回模型值,也可以改为返回解析错误。
import {Component, model} from '@angular/core';
import {FormValueControl, transformedValue} from '@angular/forms/signals';

@Component({
  selector: 'number-input',
  template: `
    <input type="text" [value]="rawValue()" (input)="rawValue.set($event.target.value)" />
  `,
})
export class NumberInput implements FormValueControl<number | null> {
  readonly value = model.required<number | null>();

  protected readonly rawValue = transformedValue(this.value, {
    parse: (val: string): number => ({value: val ? Number(val) : null}),
    format: (val: number): string => val?.toString() ?? '',
  });
}

工作机制:

  • 向返回的信号写入(rawValue.set(...))会先执行 parse,把解析结果写进 value(并同步更新原始值本身);
  • 模型值被其它途径修改时——例如 reset()、某个 schema 规则、或应用其它部分写入——format 会重新执行,原始值自动跟上模型值。

源码层面,transformedValueapi/transformed_value.ts)用一个 linkedSignal 基于 format(value()) 派生原始值,并基于 createParser(value, value.set, parse) 创建内部解析器,同时把返回信号包装为带 parseErrors 属性的 TransformedValueSignal。它还向「最近的字段」注入了内部 FORM_CONTROL_INTEGRATION token 用于上报解析错误与复位(详见下两节)。

上报解析错误(parse errors)

有时原始值没有合法的模型表示——日期只输入了一半,或数字字段混入了字母。上面的 NumberInput 就有此隐患:Number('abc') 结果是 NaNparse 却把它「愉快地」写进了模型。

改为返回 {error}

export class NumberInput implements FormValueControl<number | null> {
  readonly value = model.required<number | null>();

  protected readonly rawValue = transformedValue(this.value, {
    parse: (val) => {
      const parsed = val ? Number(val) : null;

      return Number.isNaN(parsed)
        ? {error: {kind: 'parse', message: `${val} is not a number`}}
        : {value: parsed};
    },
    format: (val) => val?.toString() ?? '',
  });
}

parse 的返回值(即 ParseResult<TValue>,见 transformed_value.ts)可以包含:

  • value:解析后的模型值;省略时模型不更新;
  • error:解析中遇到的问题,可以是单个或多个错误对象(OneOrMany<...>);
  • 两者同时返回:既更新模型,又标记一个待处理问题。

当控件以 [formField] 绑定时,解析错误会被自动上报到字段,从而与校验错误一起出现在字段的 errors() 信号里:

@Component({
  imports: [NumberInput, FormField],
  template: `
    <number-input [formField]="orderForm.amount" />

    @for (error of orderForm.amount().errors(); track $index) {
      <!-- {kind: 'parse', message: '...'} is reported here too -->
      <p class="error">{{ error.message }}</p>
    }
  `,
})
export class Order {
  orderModel = signal<{amount: number | null}>({amount: null});
  orderForm = form(this.orderModel);
}

携带解析错误的字段视同无效,与未通过校验规则一样会阻断表单提交。这条「自动上报」链路在 transformed_value.ts 中实现:当组件处于字段上下文(注入到 FORM_CONTROL_INTEGRATION)时,解析器错误通过 integration.setParseErrors(parser.errors) 交给 FormField 合并进 errors()

HELPFUL:原生输入也走同一套机制。当浏览器无法解析某值时(例如 <input type="date"> 中的半截日期),它同样会以字段上的 parse 错误呈现,详见 Native HTML validation

重置控件

对字段调用 reset() 会清除挂起的解析错误,并从模型值重新执行 format,使一个停留在「不可解析状态」的控件回到干净的可展示值:

orderForm.amount().reset();

在源码层面,FormField.reset()form_field.ts)会调用注册的 resetter,并通过 parseErrorsResetCallback 通知 transformedValue 执行内部解析器复位 parser.reset() 与重新格式化。此机制的回归测试可在 test/node/parse_errors.spec.tstest/web/number_input.spec.ts 中找到。

校验集成:控件展示结果,schema 负责判定

自定义控件展示校验状态,但不执行校验。校验发生在表单 schema 中——控件通过 [formField] 指令接收 invalid()errors() 信号并渲染(如前面 StatefulInput 所示)。

[formField] 指令还会透传 requiredminmaxminLengthmaxLengthpattern 等校验约束值,控件可以用它们增强 UI(例如把约束映射为原生 HTML 属性或提示文案):

export class NumberInput implements FormValueControl<number> {
  value = model<number>(0);

  // Constraint values from schema validation rules
  required = input<boolean>(false);
  min = input<number | undefined>(undefined);
  max = input<number | undefined>(undefined);
}

当你在 schema 中加入 min() / max() 校验规则后,[formField] 指令会把这些约束值推送到控件的对应输入。该透传循环的源码实现在 control_custom.ts:对 CONTROL_BINDING_NAMES 中的每个状态(含 errorsrequiredminmaxminLengthmaxLengthpattern 等)读取字段状态值,仅在值变化时通过 host.setInputOnDirectives(name, value) 写入控件输入信号。

IMPORTANT:不要把校验逻辑写在控件里。应在表单 schema 中定义校验规则,让控件只负责展示结果:

// Avoid: Validation in control
export class BadControl implements FormValueControl<string> {
  value = model<string>('');
  isValid() {
    return this.value().length >= 8;
  } // Don't do this!
}
// Good: Validation in schema, control displays results
accountForm = form(this.accountModel, (schemaPath) => {
  minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'});
});

一条补充的实现细节:FormField.errorsform_field.ts)在返回字段错误时会按 formField 归属过滤,确保每个控件看到的是与自身绑定相关的错误集合。

让控件可复用:配套 schema 与 apply() 组合

自定义控件往往隐含着对校验的预期。一个邮箱输入框几乎总需要 required + email 规则,若依赖每个使用方反复声明会非常繁琐。更优雅的做法是:把配套 schema 与控件打包在同一个模块里一并导出。

// email-input.ts
import {schema, required, email} from '@angular/forms/signals';

export const emailFieldSchema = schema<string>((path) => {
  required(path, {message: 'Email is required'});
  email(path, {message: 'Enter a valid email address'});
});

消费方导入配套 schema,并用 apply() 把它组合进自己的表单:

// registration.ts
import {form, apply} from '@angular/forms/signals';
import {emailFieldSchema} from './email-input';

registrationForm = form(this.registrationModel, (path) => {
  apply(path.email, emailFieldSchema);
});

apply() 会把配套 schema 的规则合并到父表单的指定路径下,且采用组合而非替换语义——消费方仍可在同一字段上追加更多规则。schema()apply() 以及条件组合 applyWhen() 的完整用法见 Schemas 指南

可复用设计注意事项

  • 初始值必须定义:消费方的模型必须为每个字段初始化为有定义的值。在 Signal Forms 中,undefined 表示「字段不存在」而非「空值」。因此可复用的邮箱控件要求消费方以 '' 初始化该属性,不能让它保持 undefined。初始值选型细节见 Form Models 指南
  • 不要自己注册 effect:字段状态由表单系统通过内部 effect 管理,控件通过输入信号接收状态更新。如果控件需要转换值,请使用上文 transformedValue()(见「值转换」一节),而不是自行调用 effect()——自己注册 effect 会绕过表单系统的管理并造成重复的副作用执行。

从样例到工程:常见问题速查

需求 做法 依据
单值输入控件 实现 FormValueControl,提供 value = model(...) control.ts
开关/布尔控件 实现 FormCheckboxControl,提供 checked = model(...) control.ts
展示校验错误/禁用态 声明 errorsinvaliddisabled 等可选 input 见「可选状态属性」表格
支持 debounce('blur') 提供 touch = output(),在原生 blur 时 emit control_custom.ts
UI 值 ≠ 模型值 transformedValue(value, {parse, format}) transformed_value.ts
输入不可解析 parse 返回 {error: {kind: 'parse', message}} 见「Reporting parse errors」
内置校验规则跟随控件 同文件导出 schema(...),消费方 apply() 见「Making controls reusable」
校验判定 一律放表单 schema,控件只渲染 见「Validation integration」

下一步

本文完整覆盖了构建与 Signal Forms 深度集成的自定义控件的全部关键链路:控制接口契约(含互斥的 value/checked 约束)、[formField] 指令的检测与绑定机制、可选状态信号、touchdebounce('blur') 的协作、transformedValue 的值转换与解析错误上报,以及用配套 schema 实现控件复用。同一主题的其余方面可继续查阅:

如果你正把现有响应式表单迁移到 Signal Forms,可参考 Migration 指南;Signal Forms 自带的 FormField 同时支持原生元素、实现 FormValueControl/FormCheckboxControl 的自定义控件,以及(仅为向后兼容而保留的)ControlValueAccessor,这也意味着新的自定义控件编写方式可以与旧生态共存。编写此类控件与指令的端到端行为验证,可进一步阅读仓库中的相关测试,例如 test/web/form_field.spec.tstest/web/number_input.spec.tstest/node/parse_errors.spec.ts

登录后查看全文
热门项目推荐
相关项目推荐