mcpField
F
falcondev-form

falcondev-form

github.com/falcondev-oss开发288 次安装5 天前更新
代码仓库

关于 falcondev-form

基于 `@vue/reactivity` 的框架无关响应式表单状态库,提供 React 和 Vue 绑定。它使用 Standard Schema 校验,并以惰性字段访问器管理可空表单数据。

主要功能

  1. 01Standard Schema 校验
  2. 02响应式表单与字段状态
  3. 03惰性类型化字段访问器
  4. 04可空编辑与校验后提交
  5. 05React 和 Vue 绑定

适用场景

  1. 01构建经 Schema 校验的 React 表单
  2. 02构建经 Schema 校验的 Vue 表单
  3. 03管理嵌套字段与数组字段
  4. 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:

PackageEntryAdds
@falcondev-oss/form-coreuseFormCore、所有类型、/reactive helpers引擎;仅在 React/Vue 之外直接使用
@falcondev-oss/form-reactuseForm、useField、FormFieldMemoReact 重新渲染集成、field.model = {value,onUpdate}
@falcondev-oss/form-vueuseForm、useFormHandlesfield.model 可写计算属性(v-model)

框架软件包中的 useForm 会封装 useFormCore——选项相同,handle 相同。除字段绑定和重新渲染外,以下内容在所有框架中都相同(参见 reference/frameworks.md)。

解释一切的两个概念

  1. Accessors 是惰性路径;$use() 会将其具象化。 form.fields.address.city 只是一个带类型的路径代理——此时还没有字段对象。调用 .$use() 会创建(并缓存)实际的响应式 FormField。使用点号/.at() 导航,然后在绑定的叶节点上调用 .$use()。

  2. 表单数据是 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.dataNullableDeep<T> | undefined响应式;可直接读取和写入(form.data.x = …)。待处理期间为 undefined。
form.fields访问器树导航后调用 .$use()
form.isDirtyboolean是否有任何更改(编辑计数 ≠ 0);submit 成功或调用 reset 后重置
form.isChangedboolean当前数据与 sourceValues 是否深度相等(编辑后恢复为原值时为 false)
form.isLoadingboolean正在提交或源值仍处于待处理状态
form.isDisabledboolean正在加载或启用了 disabled 选项
form.errorsIssue[] | 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.errorsstring[] | undefined — 此字段的消息(以及未认领的嵌套问题)
field.schemaSchemaMeta — 架构中的 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、Vue v-model/useFormHandles、独立使用 core)→ 阅读 reference/frameworks.md。
  • 可选功能 — 判别联合($use({discriminator}))、值转换($use({translate}))、field.schema 元数据和钩子 → 阅读 reference/patterns.md。

评论

支持 Markdown