TanStack Form + Svelte 实战:从零构建一个带校验、异步验证与条件字段的简单表单
【免费下载链接】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
本篇文章以仓库中 examples/svelte/simple 这个官方最小示例为主体,完整讲解如何在 Svelte(搭配 Vite 与 TypeScript)中接入 TanStack Form,实现一个包含文本输入、复选框、条件字段、同步/异步校验、错误提示、提交状态与重置功能的真实表单。读完本文,你将掌握createForm、form.Field、form.Subscribe的核心用法,并理解它们背后在@tanstack/svelte-form与@tanstack/form-core中的底层实现。
一、示例概览:README 告诉你的第一件事
仓库中的 simple 示例 README 内容极简,只有两行命令,却是整个示例的入口:
npm install npm run dev这两条命令分别完成依赖安装与本地开发服务器启动。从 package.json 可以看到,这是一个基于Vite 7 + Svelte 5 + TypeScript 5.9的纯前端工程,唯一的运行时依赖是@tanstack/svelte-form(^1.23.0),这正是 TanStack Form 为 Svelte 提供的官方适配包;其他依赖均为构建与编译工具链:
{ "name": "@tanstack/form-example-svelte-simple", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/svelte-form": "^1.23.0" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tsconfig/svelte": "^5.0.5", "svelte": "^5.39.4", "typescript": "5.9.3", "vite": "^7.2.2" } }示例的完整目录结构如下:
examples/svelte/simple/ ├── index.html # HTML 入口,挂载点 #app ├── package.json ├── svelte.config.js # vitePreprocess 预处理配置 ├── tsconfig.json # 继承 @tsconfig/svelte ├── vite.config.ts # Vite + svelte() 插件 └── src/ ├── App.svelte # 表单主组件(本文核心) ├── FieldInfo.svelte # 字段错误信息展示组件 ├── main.ts # Svelte 挂载入口 └── vite-env.d.ts从工程结构可以看出,这个示例是一个"零路由、零样式框架"的最小化演示,所有表单逻辑都集中在 src/App.svelte 中,非常适合作为理解 TanStack Form 在 Svelte 中工作方式的第一课。
二、项目脚手架:Svelte 5 的 Vite 工程如何启动
在深入表单代码之前,先了解这个示例的启动链路,便于你本地复现。
src/main.ts 使用 Svelte 5 新的mountAPI 将App组件挂载到index.html的#app节点上:
import { mount } from 'svelte' import App from './App.svelte' const app = mount(App, { target: document.getElementById('app')!, }) export default appvite.config.ts 仅注册了 Svelte 插件,svelte.config.js 启用了vitePreprocess以支持在.svelte文件中直接写 TypeScript。tsconfig.json继承了@tsconfig/svelte的基础配置,并将src下的.ts、.js、.svelte文件纳入类型检查范围。
提示:运行
npm run build可执行生产构建,npm run preview可本地预览构建产物,这三个脚本都定义在 package.json 中。
三、createForm:声明表单的默认值与提交逻辑
一切从createForm开始。src/App.svelte 的<script>块中创建了表单实例:
const form = createForm(() => ({ defaultValues: { firstName: '', lastName: '', employed: false, jobTitle: '', }, onSubmit: async ({ value }) => { // Do something with form data alert(JSON.stringify(value)) }, }))这里有三个关键设计,理解它们比记住 API 更重要:
惰性初始化(lazy initializer):
createForm接收的是一个返回配置对象的函数而非普通对象。这正是 Svelte 5 响应式(runes)体系的要求——配置函数内部引用的信号变化会被$effect.pre追踪,从而在每次组件更新时同步最新的表单选项。从 createForm.svelte.ts 的源码可以看到,createForm内部直接new FormApi(options)实例化框架无关的核心 API,随后把Field、FormGroup、Subscribe、useSelector等 Svelte 专属能力挂载到扩展 API 上:const api = new FormApi<TFormData, ...>(options) const extendedApi: typeof api & SvelteFormApi<...> = api as never extendedApi.Field = (internal, props) => Field(internal, { ...props, form: api as never } as never) extendedApi.Subscribe = (internal, props) => Subscribe(internal, { ...props, store: api.store }) onMount(api.mount) // 类似 useRef:每次更新时同步最新选项,不产生副作用 $effect.pre(() => api.update(opts?.()))组件挂载时调用
api.mount()完成表单生命周期初始化,而$effect.pre(() => api.update(opts?.()))则保证每次渲染前选项都被刷新——这就是"配置函数"模式存在的意义。defaultValues:表单数据的初始值,这里声明了
firstName、lastName两个字符串,employed布尔值,以及jobTitle字符串。createForm的泛型会基于defaultValues推断出整个表单的数据类型,后续所有form.Field的name都会被类型系统严格约束(DeepKeys深层键推导),实现类型安全。onSubmit:提交回调,
{ value }即当前表单数据。示例中仅用alert(JSON.stringify(value))展示数据,真实项目中可在这里调用 API 接口。注意 onSubmit 被声明为async,TanStack Form 会等待其完成后才结束提交状态,这在 FormApi.ts 的提交流程中体现。
四、form.Field 与校验器:同步校验 + 防抖异步校验
表单实例的form.Field是一个可嵌套组件,通过name绑定数据路径,通过validators声明校验规则。先看第一个字段的完整写法:
<form.Field name="firstName" validators={{ onChange: ({ value }) => value.length < 3 ? 'Not long enough' : undefined, onChangeAsyncDebounceMs: 500, onChangeAsync: async ({ value }) => { await new Promise((resolve) => setTimeout(resolve, 1000)) return value.includes('error') && 'No "error" allowed in first name' }, }} > {#snippet children(field)} <div> <label for={field.name}>First Name</label> <input id={field.name} type="text" placeholder="First Name" value={field.state.value} onblur={() => field.handleBlur()} oninput={(e: Event) => { const target = e.target as HTMLInputElement field.handleChange(target.value) }} /> <FieldInfo {field} /> </div> {/snippet} </form.Field>拆解这段代码,它同时示范了 TanStack Form 的几个核心概念:
校验器(validators)的返回约定:每个校验函数返回undefined/null表示通过,返回字符串即为错误信息。这里onChange校验firstName长度不得小于 3,否则返回'Not long enough'。
异步校验与防抖:onChangeAsyncDebounceMs: 500表示用户停止输入 500ms 后才触发异步校验(避免每次按键都发请求);onChangeAsync则是一个真实模拟异步操作的校验器——等待 1000ms 后,若值包含"error"子串则返回错误信息。这个组合是真实项目中"输入停顿→请求服务端校验"的标准范式。
Snippet 插槽接收字段 API:Svelte 5 使用{#snippet children(field)}接收字段实例。field的类型是FieldApi,它同时暴露了状态(field.state.value、field.state.meta)与操作(field.handleChange、field.handleBlur),UI 完全由你掌控——这正是 TanStack Form "headless(无头)" 理念的体现:它只管理状态与校验,不渲染任何 DOM。
从底层实现看,Field.svelte 中的createField工厂函数在 Svelte 侧做了三件事:
new FieldApi(options)实例化核心字段 API,并在onMount时调用api.mount(),卸载时执行清理函数释放 meta(第 82-89 行);- 通过
useSelector对api.store建立细粒度的响应式订阅——分别订阅value、meta.isTouched、meta.isBlurred、meta.isDirty、meta.errorMap、meta.errorSourceMap、meta.isValidating(第 99-121 行); - 用
Object.defineProperty为扩展 API 定义state的 getter,把所有订阅的响应式源聚合为可被 Svelte 追踪的field.state(第 122-153 行)。这就是模板中直接读取field.state.value、field.state.meta.errors就能自动响应更新的原因。
五、FieldInfo:把 meta 渲染成错误提示
示例把错误展示抽成了独立组件 FieldInfo.svelte,这段代码很值得收藏,因为它展示了字段元信息(meta)的正确用法:
<script lang="ts"> import type { AnyFieldApi } from '@tanstack/svelte-form' let { field }: { field: AnyFieldApi } = $props() </script> {#if field.state.meta.isTouched} {#each field.state.meta.errors as error} <em>{error}</em> {/each} {field.state.meta.isValidating ? 'Validating...' : ''} {/if}要点说明:
AnyFieldApi是官方导出的"任意字段"统一类型,适合做通用子组件;field.state.meta.isTouched表示字段是否被用户触碰过(blur 后为true),因此错误信息只在触碰后才显示,避免初始状态就满屏报错;field.state.meta.errors是当前生效的错误数组,用{#each}遍历渲染;field.state.meta.isValidating在异步校验进行中为true,此时展示'Validating...'提示,配合异步校验器可以做出"校验中"的加载反馈。
这个组件的触发链路依赖上一节提到的onblur={() => field.handleBlur()}:用户离开输入框 →handleBlur更新isTouched并触发 blur 校验 → 订阅了 meta 的stategetter 重新求值 → 模板重渲染错误信息。
六、受控输入与复选框:绑定 state 的两种形态
文本输入采用标准的"值绑定 + 事件驱动"模式:
<input value={field.state.value} oninput={(e: Event) => { const target = e.target as HTMLInputElement field.handleChange(target.value) }} onblur={() => field.handleBlur()} />field.state.value驱动显示,field.handleChange(target.value)回写状态并触发onChange校验,field.handleBlur()标记触碰状态。
复选框则利用布尔值直接取反:
<form.Field name="employed"> {#snippet children(field)} <div> <label for={field.name}>Employed?</label> <input oninput={() => field.handleChange(!field.state.value)} checked={field.state.value} onblur={() => field.handleBlur()} id={field.name} type="checkbox" /> </div> ... {/snippet} </form.Field>handleChange(!field.state.value)每次点击时写入相反值,checked属性负责回显。注意employed字段没有配置validators,说明校验器是可选配置。
七、条件字段:根据状态动态渲染子字段
示例最精彩的部分是条件字段——只有勾选了 "Employed?" 复选框才显示 "Job Title" 输入框,且该输入框有必填校验:
{#if field.state.value} <form.Field name="jobTitle" validators={{ onChange: ({ value }) => value.length === 0 ? 'If you have a job, you need a title' : null, }} > {#snippet children(field)} <div> <label for={field.name}>Job Title</label> <input type="text" id={field.name} placeholder="Job Title" value={field.state.value} onblur={field.handleBlur} oninput={(e: Event) => { const target = e.target as HTMLInputElement field.handleChange(target.value) }} /> <FieldInfo {field} /> </div> {/snippet} </form.Field> {/if}这里的关键机制:
- 外层
field是employed字段,{#if field.state.value}用其响应式状态控制内部字段的挂载与卸载; form.Field是可嵌套的——内层字段的name="jobTitle"是相对整个表单数据根路径的(defaultValues.jobTitle),TanStack Form 支持任意层级的字段嵌套,无需手动管理父子关系;- 该字段的校验器规定
jobTitle为空时返回错误'If you have a job, you need a title'。由于canSubmit会自动汇总所有已挂载字段的错误状态,未勾选 "Employed?" 时jobTitle字段根本不存在,自然不会阻塞提交;一旦勾选,它就参与校验并影响提交可用性。
这正是 composition 示例 在 React 侧演示的同一能力——字段的组合与解构完全由渲染逻辑决定。
八、form.Subscribe 与提交按钮:canSubmit / isSubmitting / reset
表单提交区用form.Subscribe订阅表单级状态:
<div> <form.Subscribe selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting, })} > {#snippet children({ canSubmit, isSubmitting })} <button type="submit" disabled={!canSubmit}> {isSubmitting ? 'Submitting' : 'Submit'} </button> {/snippet} </form.Subscribe> <button type="button" id="reset" onclick={() => { form.reset() }} > Reset </button> </div>selector 派生订阅:selector从整个表单状态中只挑选canSubmit(能否提交,由所有字段校验与表单校验共同决定)和isSubmitting(是否正在提交)两个字段,形成派生状态对象。提交按钮在!canSubmit时禁用,提交过程中文案切换为'Submitting'。这种"按需订阅"是 TanStack Form 性能设计的一部分——只有被 selector 选中的状态变化才会触发重渲染。底层 Subscribe.svelte 实现非常薄:它调用useSelector(store, selector)获取派生值,再{@render children(value.current)}渲染传入的 snippet。
表单提交链路:<form>元素的onsubmit处理器如下:
<form id="form" onsubmit={(e) => { e.preventDefault() e.stopPropagation() form.handleSubmit() }} >handleSubmit()会依次执行:字段校验 → 表单级校验(若配置了validators)→ 置isSubmitting为true→ 调用onSubmit→ 完成后复位isSubmitting。
重置:form.reset()一键把表单数据恢复为defaultValues,同时清空字段的触碰/错误等 meta 状态。
九、运行效果与学习路径
执行npm install && npm run dev后,浏览器会打开一个TanStack Form - Svelte Demo页面:输入少于 3 个字符的姓名会立刻提示Not long enough;在 First Name 中输入包含error的文本,停顿 500ms 后进入 1 秒的异步校验并显示Validating...,随后提示No "error" allowed in first name;勾选 Employed? 出现 Job Title 必填项;提交按钮在存在任何错误时禁用,提交期间文案变为Submitting;Reset 按钮可一键还原表单。
这个 100 行出头的示例实际上覆盖了 TanStack Form 在 Svelte 中最常用的一整套 API。进一步学习可以沿着以下路径深入仓库:
- 框架无关的核心状态机:packages/form-core/src/FormApi.ts 与 packages/form-core/src/FieldApi.ts,理解
handleChange、handleBlur、校验队列的实现; - Svelte 适配层完整源码:packages/svelte-form/src/createForm.svelte.ts、Field.svelte、Subscribe.svelte;
- 更多实战场景:仓库 examples/svelte 下还提供了
array(数组字段)、large-form(大型表单性能)、multi-step-wizard(多步向导)、standard-schema(标准 Schema 校验)等示例; - 官方文档入口:Svelte 快速上手、Svelte 指南目录、Svelte 参考目录。
从最小示例出发,逐步对照源码阅读,是理解 TanStack Form 这套"框架无关核心 + 各框架薄适配层"架构最有效的路径。
【免费下载链接】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),仅供参考