关于 falcondev-form
基于 `@vue/reactivity` 的框架无关响应式表单状态库,提供 React 和 Vue 绑定。它使用 Standard Schema 校验,并以惰性字段访问器管理可空表单数据。
主要功能
- 01Standard Schema 校验
- 02响应式表单与字段状态
- 03惰性类型化字段访问器
- 04可空编辑与校验后提交
- 05React 和 Vue 绑定
适用场景
- 01构建经 Schema 校验的 React 表单
- 02构建经 Schema 校验的 Vue 表单
- 03管理嵌套字段与数组字段
- 04处理提交前的可空编辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ falcondev-oss/ skills --skill falcondev-form安装后在 Claude Code 中输入/falcondev-form确认已加载。
附带文件reference/frameworks.mdreference/patterns.md
详细介绍
@falcondev-oss/form
一个构建于 @vue/reactivity 之上的框架无关表单状态库。提供一个 Standard Schema(zod v4、arktype 等)和响应式 sourceValues,即可获得一个带有字段 accessors 树的响应式表单 handle。
发布了三个软件包——根据运行时选择对应的软件包,切勿在应用代码中直接导入 -core:
| Package | Entry | Adds |
|---|---|---|
@falcondev-oss/form-core | useFormCore、所有类型、/reactive helpers | 引擎;仅在 React/Vue 之外直接使用 |
@falcondev-oss/form-react | useForm、useField、FormFieldMemo | React 重新渲染集成、field.model = {value,onUpdate} |
@falcondev-oss/form-vue | useForm、useFormHandles | field.model 可写计算属性(v-model) |
框架软件包中的 useForm 会封装 useFormCore——选项相同,handle 相同。除字段绑定和重新渲染外,以下内容在所有框架中都相同(参见 reference/frameworks.md)。
解释一切的两个概念
-
Accessors 是惰性路径;
$use()会将其具象化。form.fields.address.city只是一个带类型的路径代理——此时还没有字段对象。调用.$use()会创建(并缓存)实际的响应式FormField。使用点号/.at()导航,然后在绑定的叶节点上调用.$use()。 -
表单数据是
NullableDeep。 编辑期间,每个值都可以是null——填写了一半的表单中到处都是 null。因此,form.data、field.value和 accessor 类型都是架构的 input 类型,并将其深度设为可空(对象 →T | null,数组 →(T|null)[] | null)。经过验证的 output 类型(非 null)只会出现在submit({ values })中。UI 应按可空类型编写;干净的数据则由submit保证。
最简路径
import { useForm } from '@falcondev-oss/form-vue' // or -react
import z from 'zod'
const form = useForm({
schema: z.object({ name: z.string(), age: z.number() }),
sourceValues, // initial data (NullableDeep); a value, getter, or ref — React needs a stable reference
async submit({ values }) {
// `values` is validated OUTPUT: { name: string, age: number }
await api.save(values)
// return nothing → { success: true }; return { success: false } to keep form dirty
},
})
const nameField = form.fields.name.$use()
nameField.handleChange('Jane') // programmatic write (validates, marks dirty)
// or write straight through the reactive data:
form.data.name = 'Jane' // equivalent effect
await form.submit() // validates whole schema, runs submit if valid
useForm(options)
schema— 任意同时公开 Standard JSON Schema 的 Standard Schema(zod v4、arktype)。JSON Schema 用于驱动field.schema元数据和必填项检测。无法用 JSON 表示的类型(Date、bigint、Map 等)需要编解码器/转换,否则元数据提取会静默降级(并显示控制台警告)。sourceValues— 初始NullableDeep数据。可以是一个值、getter() => data,或(Vue)一个ref。在 React 中必须是稳定引用——参见下文。可以为undefined→ 表单会处于待处理状态(isLoading/field.isPending为 true,data为undefined,写入会被忽略),直到值解析出来。之后值发生变化时:如果表单未修改,表单会重置为新值;如果已修改,则跳过此次更新并显示警告(提交期间除外)。submit({ values })— 异步函数,接收经过验证的 output。返回void/{success:true}→ 表单标记为未修改;返回{success:false}→ 表单仍保持已修改状态。所有必须在表单完成处理前结束的操作都要使用await——尤其是缓存/查询失效操作,这样新数据才能在表单标记为未修改前回流到sourceValues中(未等待的失效操作会导致表单基于过时数据完成处理)。使用 TanStack Query 时:可以直接在submit中执行await queryClient.invalidateQueries(...),也可以在 mutation 的onSuccess中执行失效操作,并在submit中使用await mutateAsync(...)(等待 mutation 会同时等待onSuccess)。disabled?—boolean | Ref | getter。阻止所有字段上的handleChange/handleBlur/reset。hooks?— 生命周期钩子(参见reference/patterns.md)。
编写 sourceValues(请按此方式)
三个按顺序执行的分支:仍在加载 → 已有实体 → 空白默认值。Vue 将其作为 getter 接收;React 将其作为经 useMemo 处理的值接收。
// Vue — getter, tracked reactively
useForm({
schema,
sourceValues: () => {
if (isLoading.value) return undefined // 1. source still loading → form is pending
if (entity.value) return entity.value // 2. edit: return the whole entity object
return { name: null, age: null } // 3. create: blank defaults
},
async submit({ values }) { /* … */ },
})
// React — same branches, wrapped in useMemo
const sourceValues = useMemo(() => {
if (isLoading) return undefined
if (entity) return entity
return { name: null, age: null }
}, [isLoading, entity])
useForm({ schema, sourceValues, async submit({ values }) { /* … */ } })
在 React 中,sourceValues 必须是稳定引用。 不要在 useForm 调用中直接内联对象(或 getter):React 每次渲染都会重新运行组件主体,因此新对象的身份会在每次渲染时重新初始化表单——重置未修改的表单,并对已修改的表单发出警告。请使用 useMemo 进行记忆化,或将常量提升到模块作用域,或直接传入查询数据(sourceValues: query.data——其引用已经稳定)。Getter 在这里不仅毫无用处,甚至更糟:适配器只会在挂载时捕获 getter 一次,因此基于 React 状态或属性创建的 getter 会冻结在首次渲染的闭包中,之后再也看不到更新。
- 在源数据加载期间返回
undefined(例如,正在等待获取要编辑的实体)。这样表单会进入待处理状态,而不是用错误或空白的结构初始化,之后还得再覆盖它。 - 将现有实体作为一个完整对象返回——不要用可选链为每个键手动构造
{ name: entity?.name, age: entity?.age, … }。如果实体与架构不匹配,请展开实体,仅覆盖不一致的字段:return { ...entity, tags: entity.tags ?? [] }。 - 对没有实际默认值的字段使用
null——不要仅仅因为类型是字符串或数字就使用''或0。NullableDeep让每个字段都可以使用null;空字符串是一个实际值,表示“用户什么都没输入”,这与“尚未触碰”不同。
表单句柄
| 成员 | 类型 | 说明 |
|---|---|---|
form.data | NullableDeep<T> | undefined | 响应式;可直接读取和写入(form.data.x = …)。待处理期间为 undefined。 |
form.fields | 访问器树 | 导航后调用 .$use() |
form.isDirty | boolean | 是否有任何更改(编辑计数 ≠ 0);submit 成功或调用 reset 后重置 |
form.isChanged | boolean | 当前数据与 sourceValues 是否深度相等(编辑后恢复为原值时为 false) |
form.isLoading | boolean | 正在提交或源值仍处于待处理状态 |
form.isDisabled | boolean | 正在加载或启用了 disabled 选项 |
form.errors | Issue[] | undefined | 所有验证问题,或为 undefined |
form.submit() | () => Promise<{success:boolean}> | 验证所有字段并运行 submit |
form.reset() | () => void | 恢复为 sourceValues |
form.hooks | 可挂钩 | hook/hookOnce/addHooks |
字段访问器
form.fields.user.email.$use() // nested object
form.fields.tags.at(0).$use() // array item (negative index ok: .at(-1))
form.fields.tags.$use() // the array field itself
for (const item of form.fields.tags) item.$use() // iterate (reactive)
form.fields.tags.delete(field.key) // remove array item by its key (see below)
form.fields.$use() // root field = whole form value
form.fields['a.b'].$use() // keys containing dots work (auto-escaped)
即使数组/索引尚不存在,.at(i) 访问器也存在——数据到达前,.$use().value 为 null。.delete(key) 必须针对数组项的键(items[2]),而不是嵌套属性——否则会抛出异常。
FormField($use() 的结果)
| 成员 | 说明 |
|---|---|
field.value | 字段 NullableDeep 值的只读 ref。嵌套对象属性仍可写入(写入会同步到 form.data);数组可调用 .push。 |
field.handleChange(v) | 设置值、验证、标记为已修改并触发字段更改钩子 |
field.handleBlur() | 如果字段已编辑,则进行验证(用于输入框失焦时) |
field.reset() | 将此字段恢复为源值 |
field.errors | string[] | undefined — 此字段的消息(以及未认领的嵌套问题) |
field.schema | SchemaMeta — 架构中的 required、title、min/maxLength、minimum/maximum 等(参见模式) |
field.disabled / field.isPending | 与表单状态保持一致 |
field.isDirty / field.isChanged | 字段级状态,语义与表单相同 |
field.path | 点号/方括号路径字符串 |
field.key | 稳定唯一 ID(path@timestamp-rand);用作列表的 :key 和 array.delete() 的参数 |
field.model | 框架绑定——Vue:可写 computed(v-model);React:{ value, onUpdate }。参见框架。 |
field.$() | 从字段获取访问器树(用于访问经 $use 处理的字段的子项) |
两种写入方式(等效)
field.handleChange(newValue)— 显式调用,由输入处理器使用。form.data.path = newValue— 直接进行响应式修改;on-change会观察到修改并触发相同的验证。
这两种方式都会将表单标记为已修改并重新验证。在组件中使用 handleChange/model;直接写入则适用于测试和副作用。
验证何时运行
先验证整个架构(而非逐个字段),然后将问题筛选到各字段的路径:
- 提交时 — 始终运行;
- 更改时 — 仅当字段已有错误时运行(因此修正错误后会实时清除错误);
- 失焦时 — 字段已编辑时运行。
isLoading 时会清除错误。字段也会显示尚未通过 $use 使用的嵌套路径上的验证问题。
注意事项
- 读取/写入时允许为 null,提交时不允许。 不要假设 UI 中的
field.value不为 null。 - 必须使用
$use()进行绑定。 访问器本身只是无操作的路径。在 React 中,$use()会触发拥有useForm的组件重新渲染;读取字段属性的子组件需要使用useField(field)或FormFieldMemo才会重新渲染(参见框架相关说明)。 sourceValues不会覆盖已修改的表单(这是有意设计的)——如果需要强制覆盖,请先重置。delete(key)需要数组项的键,并且通过handleChange替换数组后,field.key会发生变化(缓存会被清除)。- 不要在 React/Vue 应用中从
-core导入内容——否则会失去重新渲染/model集成。
深入参考
- 框架特定内容(React 重新渲染/
useField/FormFieldMemo、Vuev-model/useFormHandles、独立使用 core)→ 阅读reference/frameworks.md。 - 可选功能 — 判别联合(
$use({discriminator}))、值转换($use({translate}))、field.schema元数据和钩子 → 阅读reference/patterns.md。