ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Solid Form 字段联动校验:用 onChangeListenTo / onBlurListenTo 连接两个表单字段

2026/9/17 9:42:46 拓冰建站 浏览量
Solid Form 字段联动校验:用 onChangeListenTo / onBlurListenTo 连接两个表单字段 Solid Form 字段联动校验用 onChangeListenTo / onBlurListenTo 连接两个表单字段【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form本文导读在使用 TanStack Form 的 Solid 适配器Solid Form构建表单时经常需要处理「两个字段互相约束」的场景——最典型的就是password与confirm_password无论用户先修改哪一个字段另一个字段的校验都必须立即重新执行。本文围绕 docs/framework/solid/guides/linked-fields.md 中介绍的onChangeListenTo/onBlurListenTo配置讲解字段联动校验的完整解决方案并深入到 FieldApi.ts 与 FieldGroupApi.ts 的源码实现帮助读者理解监听其他字段这一机制的工作原理、适用边界与实战写法。读完本文你将能够编写密码确认这类双向关联字段理解联动校验的触发链路掌握字段组FieldGroup内相对字段名的重映射规则。一、问题场景为什么两个字段必须一起校验表单中经常出现一类需求一个字段的校验结果依赖于另一个字段的当前值。官方指南在 linked-fields.md 中给出了最典型的使用场景——密码确认框表单同时包含password密码与confirm_password确认密码两个字段当confirm_password的值与password不一致时confirm_password必须报错无论用户先修改哪个字段confirm_password的校验都需要感知到对方的变更。想象下面的用户操作流程用户先修改了confirm_password字段此时触发的是confirm_password自身的onChange校验用户随后又修改了password字段此时触发的是password自身的onChange校验。如果不对字段做任何联动处理第二种操作结束后表单里依然残留着confirm_password的校验错误——因为确认密码字段的校验逻辑根本没有被重新执行过它并不知道password已经变成了新值。这就是字段联动校验要解决的核心问题当目标字段password的值发生变化时需要强制重新运行依赖字段confirm_password的校验。二、解决方案为confirm_password添加onChangeListenToTanStack Form 为这个问题提供了内建的能力在字段的validators配置中添加onChangeListenTo属性其值为一个字段名数组表示当数组中任意一个字段的值发生变化时当前字段的onChange以及onChangeAsync校验会被重新触发。官方文档给出的完整示例Solid 适配器写法如下export default function App() { const form createForm(() ({ defaultValues: { password: , confirm_password: , }, // ... })) return ( div form.Field namepassword {(field) ( label divPassword/div input value{field().state.value} onChange{(e) field().handleChange(e.target.value)} / /label )} /form.Field form.Field nameconfirm_password validators{{ onChangeListenTo: [password], onChange: ({ value, fieldApi }) { if (value ! fieldApi.form.getFieldValue(password)) { return Passwords do not match } return undefined }, }} {(field) ( div label divConfirm Password/div input value{field().state.value} onChange{(e) field().handleChange(e.target.value)} / /label Index each{field().state.meta.errors} {(err) div{err()}/div} /Index /div )} /form.Field /div ) }这段代码的关键点拆解onChangeListenTo: [password]声明confirm_password字段监听password字段。此后无论password因何种方式被修改用户输入、handleChange、setFieldValue等confirm_password的onChange校验都会重新运行。onChange校验函数从参数解构出value当前字段即confirm_password的值和fieldApi通过fieldApi.form.getFieldValue(password)读取表单中password字段的当前值两者不等即返回错误信息Passwords do not match相等则返回undefined表示通过。错误渲染使用 Solid 的Index组件遍历field().state.meta.errors把该校验产生的错误信息渲染到界面上。同类的onBlurListenTo官方文档指出这一机制同样适用于onBlurListenTo属性它会当被监听的字段发生 blur失焦时重新运行当前字段的onBlur以及onBlurAsync校验。使用方式与onChangeListenTo完全一致validators{{ onBlurListenTo: [password], onBlur: ({ value, fieldApi }) { if (value ! fieldApi.form.getFieldValue(password)) { return Passwords do not match } return undefined }, }}从类型定义上可以看到两者的语义是并列的见 FieldApi.ts/** * An optional list of field names that should trigger this fields * onChange and onChangeAsync events when its value changes */ onChangeListenTo?: DeepKeysTParentData[] /** * An optional list of field names that should trigger this fields * onBlur and onBlurAsync events when its value changes */ onBlurListenTo?: DeepKeysTParentData[]注意DeepKeysTParentData[]的类型约束字段名数组中的元素是深层键也就是说可以监听嵌套在对象中的字段例如[profile.password]并且会经过 TypeScript 的类型推导校验拼错字段名会在编译期报错——这正是 TanStack Form 强调的 type-safe 特性在联动场景下的体现。三、原理深入联动校验在源码中是如何被触发的理解了用法之后再来看这一机制在 form-core 中的实现能够帮助你更准确地预判它的行为边界。3.1 核心实现getLinkedFields当任意字段触发了change或blur类型的校验时校验入口会调用getLinkedFields(cause)找出所有监听当前字段的字段。其实现位于 FieldApi.tsgetLinkedFields (cause: ValidationCause) { const fields Object.values(this.form.fieldInfo) as FieldInfoany[] const linkedFields: AnyFieldApi[] [] for (const field of fields) { if (!field.instance) continue if (!(field.instance instanceof FieldApi)) { continue } const { onChangeListenTo, onBlurListenTo } field.instance.options.validators || {} if (cause change onChangeListenTo?.includes(this.name)) { linkedFields.push(field.instance) } if (cause blur onBlurListenTo?.includes(this.name as string)) { linkedFields.push(field.instance) } } return linkedFields }这段代码说明了几个关键事实它会遍历表单注册的所有字段this.form.fieldInfo逐个检查它们的validators.onChangeListenTo/onBlurListenTo数组中是否包含当前字段的名字this.name匹配方式与校验触发源有关当cause是change时只匹配onChangeListenTo当cause是blur时只匹配onBlurListenTo匹配到的字段会被收集为关联字段随后参与校验执行。3.2 校验执行同步与异步双通道拿到关联字段后无论是同步校验还是异步校验都会把关联字段的校验函数一并纳入执行。同步路径在validateSync中见 FieldApi.ts它先构造当前字段的校验器数组再通过getLinkedFields(cause)拿到关联字段把每个关联字段的同cause校验器收集起来最后在同一个batch中逐一执行并写入各自的meta.errorMap。异步路径在validateAsync中见 FieldApi.ts实现逻辑与同步版本对称同样先取关联字段的异步校验器再统一构造 Promise 列表等待执行。也就是说onChangeListenTo/onBlurListenTo同时作用于同步校验onChange/onBlur与异步校验onChangeAsync/onBlurAsync这与 FieldApi.ts 中触发onChange和onChangeAsync事件的类型注释是相互印证的。3.3 一个值得注意的实现细节从getLinkedFields的源码可以看到一行注释// TODO: How to handle FieldGroups? Do we need to? IDK. if (!(field.instance instanceof FieldApi)) { continue }即当前实现只处理普通的FieldApi实例会跳过字段组FieldGroup相关的实例。这一边界在form-core层是已知的使用时如果涉及字段组嵌套建议以普通字段之间联动为主并留意后续版本对此处的演进。四、在字段组FieldGroup中使用字段名自动重映射Solid Form 支持通过createFormHookwithFieldGroup把表单按区块拆分详见 createFormHook.tsx。在字段组内部onChangeListenTo/onBlurListenTo仍然可用并且框架会自动把组内相对字段名重映射为表单全局字段名。这一逻辑位于 FieldGroupApi.ts当校验器中包含onChangeListenTo或onBlurListenTo时会调用内部的remapListenTo将每个局部字段名通过this.getFormFieldName(localFieldName)拼接上组前缀如account.再写回校验器配置。该行为在 packages/solid-form/tests/createFormHook.test.tsx 中有明确的测试覆盖。例如一个名为account的字段组内group.Field namepassword validators{{ onChange: () null, onChangeListenTo: [password], onBlur: () null, onBlurListenTo: [confirmPassword], }} {(field) { // 期望onChangeListenTo 被重映射为 [account.password] // 期望onBlurListenTo 被重映射为 [account.confirmPassword] return / }} /group.Field测试断言field().options.validators?.onChangeListenTo等于[account.password]、onBlurListenTo等于[account.confirmPassword]当字段组以对象映射方式fields{{ password: userPassword, confirmPassword: userConfirmPassword }}重命名字段时也会相应重映射为[userPassword]/[userConfirmPassword]。这意味着在字段组内写联动监听时你只需要关注组内的局部字段名框架会自动翻译成表单全局字段名不会因为嵌套而失效。五、实战要点与边界提醒结合官方指南与源码实现汇总以下实战建议字段名必须精确匹配onChangeListenTo数组中的字段名要与被监听字段在表单中的实际 name 完全一致顶层字段就是password这样的直接名字嵌套字段需要用DeepKeys点路径形式。Solid 侧的 Field 组件类型定义见 createField.tsx字段名数组的类型为DeepKeysTLensData[]会做类型约束。联动是被动触发而非主动双向绑定onChangeListenTo的语义是当 A 变化时重跑 B 的校验它不会反向修改 B 的值或状态。密码确认这类以 B 校验 A 与 B 是否相等的需求应当把校验逻辑放在 Bconfirm_password上并让它监听 Apassword。错误信息读取方式Solid 适配器中校验错误统一从field().state.meta.errors读取并用Index渲染本文示例中Passwords do not match这样的字符串会进入错误数组。异步校验同样生效如果你在联动字段上使用了onChangeAsync或onBlurAsync联动触发时异步校验也会一并重跑因此无需为联动场景额外编写两套逻辑。字段组内的相对名自动重映射在 FieldGroup 中使用时直接写组内局部字段名即可框架负责拼接组前缀见 FieldGroupApi.ts。当前实现边界form-core的getLinkedFields目前只对FieldApi实例生效字段组实例会被跳过见 FieldApi.ts 中的 TODO 注释涉及字段组本身作为联动对象时需注意。六、小结onChangeListenTo与onBlurListenTo是 TanStack Form 处理字段间相互依赖校验的声明式方案只需要在依赖方字段上声明我监听谁框架就会在被监听字段每次变更change或失焦blur时自动重跑依赖方的同步/异步校验。本文以password/confirm_password为例演示了完整用法并从 FieldApi.ts 的getLinkedFields、validateSync/validateAsync实现出发说明了联动触发的内部链路还通过 createFormHook.test.tsx 的测试用例验证了字段组内字段名的自动重映射行为。掌握了这套机制你就可以在任何一个字段的值决定另一个字段的校验结果的场景中写出声明式、类型安全且无需手动协调多次校验的 Solid 表单代码。【免费下载链接】form Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, and Lit.项目地址: https://gitcode.com/GitHub_Trending/form/form创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考