formisch
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFormisch
Formisch
This skill helps AI agents work effectively with Formisch, the schema-based, headless form library for modern frameworks.
本技能可帮助AI Agent高效使用Formisch——一款基于Schema的无UI表单库,适用于各类现代前端框架。
When to Use This Skill
适用场景
- When the user asks about form handling with Formisch
- When managing form state and validation
- When working with React, Vue, Solid, Preact, Svelte, or Qwik forms
- When integrating Valibot schemas with forms
- 当用户咨询如何使用Formisch处理表单时
- 管理表单状态与验证逻辑时
- 开发React、Vue、Solid、Preact、Svelte或Qwik框架下的表单时
- 集成Valibot Schema与表单时
Introduction
简介
Formisch is a schema-based, headless form library that works across multiple frameworks. Key highlights:
- Small bundle size — Starting at ~2.5 kB
- Schema-based validation — Uses Valibot for type-safe validation
- Headless design — You control the UI completely
- Type safety — Full TypeScript support with autocompletion
- Framework-native — Native performance for each supported framework
Formisch是一款跨多框架的Schema优先无UI表单库,核心特性如下:
- 体积小巧 —— 最小仅约2.5 kB
- Schema驱动验证 —— 基于Valibot实现类型安全的验证
- 无UI设计 —— 完全由开发者控制表单UI
- 类型安全 —— 完整支持TypeScript与自动补全
- 框架原生性能 —— 为每个支持的框架提供原生级性能
Supported Frameworks
支持的框架
| Framework | Package | Hook/Primitive |
|---|---|---|
| React | | |
| Vue | | |
| SolidJS | | |
| Preact | | |
| Svelte | | |
| Qwik | | |
| 框架 | 包名 | Hook/基础组件 |
|---|---|---|
| React | | |
| Vue | | |
| SolidJS | | |
| Preact | | |
| Svelte | | |
| Qwik | | |
Installation
安装步骤
1. Install Valibot (peer dependency)
1. 安装Valibot(依赖包)
bash
npm install valibotbash
npm install valibot2. Install Formisch for your framework
2. 安装对应框架的Formisch包
bash
npm install @formisch/react # React
npm install @formisch/vue # Vue
npm install @formisch/solid # SolidJS
npm install @formisch/preact # Preact
npm install @formisch/svelte # Svelte
npm install @formisch/qwik # Qwikbash
npm install @formisch/react # React
npm install @formisch/vue # Vue
npm install @formisch/solid # SolidJS
npm install @formisch/preact # Preact
npm install @formisch/svelte # Svelte
npm install @formisch/qwik # QwikCore Concepts
核心概念
Schema-First Design
Schema优先设计
Every form starts with a Valibot schema. Types are automatically inferred from the schema.
ts
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(
v.string("Please enter your email."),
v.nonEmpty("Please enter your email."),
v.email("The email address is badly formatted."),
),
password: v.pipe(
v.string("Please enter your password."),
v.nonEmpty("Please enter your password."),
v.minLength(8, "Your password must have 8 characters or more."),
),
});每个表单都从Valibot Schema开始,类型会自动从Schema中推导。
ts
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(
v.string("请输入您的邮箱。"),
v.nonEmpty("请输入您的邮箱。"),
v.email("邮箱格式不正确。"),
),
password: v.pipe(
v.string("请输入您的密码。"),
v.nonEmpty("请输入您的密码。"),
v.minLength(8, "密码长度至少为8位。"),
),
});Form Store
表单存储(Form Store)
The form store manages all form state. Access it via the framework-specific hook/primitive.
Form Store Properties:
- — Form is currently being submitted
isSubmitting - — Form has been successfully submitted
isSubmitted - — Validation is in progress
isValidating - — At least one field has been touched
isTouched - — At least one field differs from initial value
isDirty - — All fields pass validation
isValid - — Root-level validation errors
errors
表单存储管理所有表单状态,可通过框架专属的Hook/基础组件访问。
表单存储属性:
- —— 表单正在提交中
isSubmitting - —— 表单已成功提交
isSubmitted - —— 正在执行验证
isValidating - —— 至少有一个字段已被触碰
isTouched - —— 至少有一个字段值与初始值不同
isDirty - —— 所有字段均通过验证
isValid - —— 根级别的验证错误
errors
Field Store
字段存储(Field Store)
Each field has its own reactive store with:
- — Path array to the field
path - — Current field value
input - — Field-specific errors
errors - — Field has been focused and blurred
isTouched - — Field value differs from initial value
isDirty - — Field passes validation
isValid - — Props to spread onto input elements
props - (React) /
onChange(other frameworks) — Sets the field input value programmatically. Use this when the field cannot be connected to a native HTML element.onInput
每个字段都有独立的响应式存储,包含以下属性:
- —— 字段的路径数组
path - —— 当前字段值
input - —— 字段专属的错误信息
errors - —— 字段已被聚焦并失焦
isTouched - —— 字段值与初始值不同
isDirty - —— 字段通过验证
isValid - —— 可直接扩散到输入元素的属性
props - (React) /
onChange(其他框架) —— 以编程方式设置字段值,适用于无法关联原生HTML元素的字段onInput
Dirty Tracking
脏值追踪
Formisch tracks two inputs per field:
- Initial input — Baseline for dirty tracking (server state)
- Current input — What the user is editing (client state)
isDirtytrueFormisch为每个字段追踪两个输入值:
- 初始输入值 —— 脏值追踪的基准(服务端状态)
- 当前输入值 —— 用户正在编辑的值(客户端状态)
当当前输入值与初始输入值不同时,会变为。
isDirtytrueFramework Examples
框架示例
React Example
React 示例
tsx
import { Field, Form, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output); // { email: string, password: string }
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit" disabled={loginForm.isSubmitting}>
{loginForm.isSubmitting ? "Submitting..." : "Login"}
</button>
</Form>
);
}tsx
import { Field, Form, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output); // { email: string, password: string }
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit" disabled={loginForm.isSubmitting}>
{loginForm.isSubmitting ? "提交中..." : "登录"}
</button>
</Form>
);
}Vue Example
Vue 示例
vue
<script setup lang="ts">
import { Field, Form, useForm } from "@formisch/vue";
import type { SubmitHandler } from "@formisch/vue";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<template>
<Form :of="loginForm" @submit="handleSubmit">
<Field :of="loginForm" :path="['email']" v-slot="field">
<div>
<input v-bind="field.props" v-model="field.input" type="email" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<Field :of="loginForm" :path="['password']" v-slot="field">
<div>
<input v-bind="field.props" v-model="field.input" type="password" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<button type="submit">Login</button>
</Form>
</template>vue
<script setup lang="ts">
import { Field, Form, useForm } from "@formisch/vue";
import type { SubmitHandler } from "@formisch/vue";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<template>
<Form :of="loginForm" @submit="handleSubmit">
<Field :of="loginForm" :path="['email']" v-slot="field">
<div>
<input v-bind="field.props" v-model="field.input" type="email" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<Field :of="loginForm" :path="['password']" v-slot="field">
<div>
<input v-bind="field.props" v-model="field.input" type="password" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<button type="submit">登录</button>
</Form>
</template>SolidJS Example
SolidJS 示例
tsx
import { Field, Form, createForm } from "@formisch/solid";
import type { SubmitHandler } from "@formisch/solid";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit">Login</button>
</Form>
);
}tsx
import { Field, Form, createForm } from "@formisch/solid";
import type { SubmitHandler } from "@formisch/solid";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit">登录</button>
</Form>
);
}Svelte Example
Svelte 示例
svelte
<script lang="ts">
import { createForm, Field, Form } from '@formisch/svelte';
import type { SubmitHandler } from '@formisch/svelte';
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<Form of={loginForm} onsubmit={handleSubmit}>
<Field of={loginForm} path={['email']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="email" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<Field of={loginForm} path={['password']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="password" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<button type="submit">Login</button>
</Form>svelte
<script lang="ts">
import { createForm, Field, Form } from '@formisch/svelte';
import type { SubmitHandler } from '@formisch/svelte';
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<Form of={loginForm} onsubmit={handleSubmit}>
<Field of={loginForm} path={['email']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="email" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<Field of={loginForm} path={['password']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="password" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<button type="submit">登录</button>
</Form>Qwik Example
Qwik 示例
tsx
import { Field, Form, useForm$ } from "@formisch/qwik";
import { component$ } from "@qwik.dev/core";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default component$(() => {
const loginForm = useForm$({
schema: LoginSchema,
});
return (
<Form of={loginForm} onSubmit$={(output) => console.log(output)}>
<Field
of={loginForm}
path={["email"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="email" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<Field
of={loginForm}
path={["password"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="password" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<button type="submit">Login</button>
</Form>
);
});tsx
import { Field, Form, useForm$ } from "@formisch/qwik";
import { component$ } from "@qwik.dev/core";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default component$(() => {
const loginForm = useForm$({
schema: LoginSchema,
});
return (
<Form of={loginForm} onSubmit$={(output) => console.log(output)}>
<Field
of={loginForm}
path={["email"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="email" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<Field
of={loginForm}
path={["password"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="password" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<button type="submit">登录</button>
</Form>
);
});Form Configuration
表单配置
ts
const form = useForm({
// Required: Valibot schema
schema: MySchema,
// Optional: Initial values (partial allowed)
initialInput: {
email: "user@example.com",
},
// Optional: When first validation occurs
// Options: 'initial' | 'blur' | 'input' | 'submit' (default)
validate: "submit",
// Optional: When revalidation occurs after first validation
// Options: 'blur' | 'input' (default) | 'submit'
revalidate: "input",
});ts
const form = useForm({
// 必填:Valibot Schema
schema: MySchema,
// 可选:初始值(支持部分赋值)
initialInput: {
email: "user@example.com",
},
// 可选:首次验证触发时机
// 选项:'initial' | 'blur' | 'input' | 'submit'(默认)
validate: "submit",
// 可选:首次验证后的重验证触发时机
// 选项:'blur' | 'input'(默认) | 'submit'
revalidate: "input",
});Field Paths
字段路径
Paths are type-safe arrays that reference fields in your schema.
tsx
// Top-level field
<Field of={form} path={['email']} />
// Nested field (schema: { user: { email: string } })
<Field of={form} path={['user', 'email']} />
// Array item field (schema: { todos: [{ label: string }] })
<Field of={form} path={['todos', 0, 'label']} />
// Dynamic array index
{items.map((item, index) => (
<Field of={form} path={['todos', index, 'label']} key={item} />
))}路径是类型安全的数组,用于引用Schema中的字段。
tsx
// 顶层字段
<Field of={form} path={['email']} />
// 嵌套字段(Schema: { user: { email: string } })
<Field of={form} path={['user', 'email']} />
// 数组项字段(Schema: { todos: [{ label: string }] })
<Field of={form} path={['todos', 0, 'label']} />
// 动态数组索引
{items.map((item, index) => (
<Field of={form} path={['todos', index, 'label']} key={item} />
))}Form Methods
表单方法
All methods follow a consistent API pattern:
- First parameter: Form store
- Second parameter: Config object
所有方法遵循一致的API模式:
- 第一个参数:表单存储
- 第二个参数:配置对象
Reading Values
读取值
ts
import { getInput, getErrors, getAllErrors } from "@formisch/react";
// Get field value
const email = getInput(form, { path: ["email"] });
// Get entire form input
const allInputs = getInput(form);
// Get field errors
const emailErrors = getErrors(form, { path: ["email"] });
// Get all errors across all fields
const allErrors = getAllErrors(form);ts
import { getInput, getErrors, getAllErrors } from "@formisch/react";
// 获取单个字段值
const email = getInput(form, { path: ["email"] });
// 获取整个表单的输入值
const allInputs = getInput(form);
// 获取单个字段的错误信息
const emailErrors = getErrors(form, { path: ["email"] });
// 获取所有字段的错误信息
const allErrors = getAllErrors(form);Setting Values
设置值
ts
import { setInput, setErrors, reset } from "@formisch/react";
// Set field value (updates current input, not initial)
setInput(form, { path: ["email"], input: "new@example.com" });
// Set field errors manually
setErrors(form, { path: ["email"], errors: ["Email already taken"] });
// Clear errors
setErrors(form, { path: ["email"], errors: null });
// Reset entire form
reset(form);
// Reset with new initial values
reset(form, {
initialInput: { email: "", password: "" },
});
// Reset but keep current input
reset(form, {
initialInput: newServerData,
keepInput: true,
});ts
import { setInput, setErrors, reset } from "@formisch/react";
// 设置单个字段值(更新当前输入值,不修改初始值)
setInput(form, { path: ["email"], input: "new@example.com" });
// 手动设置字段错误
setErrors(form, { path: ["email"], errors: ["该邮箱已被注册"] });
// 清除错误
setErrors(form, { path: ["email"], errors: null });
// 重置整个表单
reset(form);
// 使用新的初始值重置表单
reset(form, {
initialInput: { email: "", password: "" },
});
// 重置表单但保留当前输入值
reset(form, {
initialInput: newServerData,
keepInput: true,
});Form Control
表单控制
ts
import { validate, focus, submit, handleSubmit } from "@formisch/react";
// Validate form manually
const isValid = await validate(form);
// Validate and focus first error field
await validate(form, { shouldFocus: true });
// Focus a specific field
focus(form, { path: ["email"] });
// Programmatically submit form
submit(form);
// Create submit handler for external buttons
const onExternalSubmit = handleSubmit(form, (output) => {
console.log(output);
});ts
import { validate, focus, submit, handleSubmit } from "@formisch/react";
// 手动验证表单
const isValid = await validate(form);
// 验证并聚焦第一个错误字段
await validate(form, { shouldFocus: true });
// 聚焦指定字段
focus(form, { path: ["email"] });
// 以编程方式提交表单
submit(form);
// 为外部按钮创建提交处理器
const onExternalSubmit = handleSubmit(form, (output) => {
console.log(output);
});Field Arrays
字段数组
For dynamic lists of fields, use with array manipulation methods.
FieldArray对于动态字段列表,可使用结合数组操作方法。
FieldArraySchema
Schema
ts
const TodoSchema = v.object({
heading: v.pipe(v.string(), v.nonEmpty()),
todos: v.pipe(
v.array(
v.object({
label: v.pipe(v.string(), v.nonEmpty()),
deadline: v.pipe(v.string(), v.nonEmpty()),
}),
),
v.nonEmpty(),
v.maxLength(10),
),
});ts
const TodoSchema = v.object({
heading: v.pipe(v.string(), v.nonEmpty()),
todos: v.pipe(
v.array(
v.object({
label: v.pipe(v.string(), v.nonEmpty()),
deadline: v.pipe(v.string(), v.nonEmpty()),
}),
),
v.nonEmpty(),
v.maxLength(10),
),
});React Example
React 示例
tsx
import {
Field,
FieldArray,
Form,
useForm,
insert,
remove,
move,
swap,
} from "@formisch/react";
export default function TodoPage() {
const todoForm = useForm({
schema: TodoSchema,
initialInput: {
heading: "",
todos: [{ label: "", deadline: "" }],
},
});
return (
<Form of={todoForm} onSubmit={(output) => console.log(output)}>
<Field of={todoForm} path={["heading"]}>
{(field) => <input {...field.props} value={field.input} type="text" />}
</Field>
<FieldArray of={todoForm} path={["todos"]}>
{(fieldArray) => (
<div>
{fieldArray.items.map((item, index) => (
<div key={item}>
<Field of={todoForm} path={["todos", index, "label"]}>
{(field) => (
<input {...field.props} value={field.input} type="text" />
)}
</Field>
<Field of={todoForm} path={["todos", index, "deadline"]}>
{(field) => (
<input {...field.props} value={field.input} type="date" />
)}
</Field>
<button
type="button"
onClick={() =>
remove(todoForm, { path: ["todos"], at: index })
}
>
Delete
</button>
</div>
))}
{fieldArray.errors && <div>{fieldArray.errors[0]}</div>}
</div>
)}
</FieldArray>
<button
type="button"
onClick={() =>
insert(todoForm, {
path: ["todos"],
initialInput: { label: "", deadline: "" },
})
}
>
Add Todo
</button>
<button type="submit">Submit</button>
</Form>
);
}tsx
import {
Field,
FieldArray,
Form,
useForm,
insert,
remove,
move,
swap,
} from "@formisch/react";
export default function TodoPage() {
const todoForm = useForm({
schema: TodoSchema,
initialInput: {
heading: "",
todos: [{ label: "", deadline: "" }],
},
});
return (
<Form of={todoForm} onSubmit={(output) => console.log(output)}>
<Field of={todoForm} path={["heading"]}>
{(field) => <input {...field.props} value={field.input} type="text" />}
</Field>
<FieldArray of={todoForm} path={["todos"]}>
{(fieldArray) => (
<div>
{fieldArray.items.map((item, index) => (
<div key={item}>
<Field of={todoForm} path={["todos", index, "label"]}>
{(field) => (
<input {...field.props} value={field.input} type="text" />
)}
</Field>
<Field of={todoForm} path={["todos", index, "deadline"]}>
{(field) => (
<input {...field.props} value={field.input} type="date" />
)}
</Field>
<button
type="button"
onClick={() =>
remove(todoForm, { path: ["todos"], at: index })
}
>
删除
</button>
</div>
))}
{fieldArray.errors && <div>{fieldArray.errors[0]}</div>}
</div>
)}
</FieldArray>
<button
type="button"
onClick={() =>
insert(todoForm, {
path: ["todos"],
initialInput: { label: "", deadline: "" },
})
}
>
添加待办
</button>
<button type="submit">提交</button>
</Form>
);
}Array Methods
数组操作方法
ts
import { insert, remove, move, swap, replace } from "@formisch/react";
// Add item at end
insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } });
// Add item at specific index
insert(form, {
path: ["todos"],
at: 0,
initialInput: { label: "", deadline: "" },
});
// Remove item at index
remove(form, { path: ["todos"], at: index });
// Move item from one index to another
move(form, { path: ["todos"], from: 0, to: 3 });
// Swap two items
swap(form, { path: ["todos"], at: 0, and: 1 });
// Replace item at index
replace(form, {
path: ["todos"],
at: 0,
initialInput: { label: "New task", deadline: "2024-12-31" },
});ts
import { insert, remove, move, swap, replace } from "@formisch/react";
// 在末尾添加项
insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } });
// 在指定索引处添加项
insert(form, {
path: ["todos"],
at: 0,
initialInput: { label: "", deadline: "" },
});
// 删除指定索引处的项
remove(form, { path: ["todos"], at: index });
// 将项从一个索引移动到另一个索引
move(form, { path: ["todos"], from: 0, to: 3 });
// 交换两个项的位置
swap(form, { path: ["todos"], at: 0, and: 1 });
// 替换指定索引处的项
replace(form, {
path: ["todos"],
at: 0,
initialInput: { label: "新任务", deadline: "2024-12-31" },
});TypeScript Integration
TypeScript 集成
Type Inference
类型推导
Types are automatically inferred from your Valibot schema:
ts
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const form = useForm({ schema: LoginSchema });
// form is FormStore<typeof LoginSchema>
// Submit handler receives typed output
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
output.email; // ✓ string
output.password; // ✓ string
output.username; // ✗ TypeScript error
};类型会自动从Valibot Schema中推导:
ts
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const form = useForm({ schema: LoginSchema });
// form 类型为 FormStore<typeof LoginSchema>
// 提交处理器接收类型化的输出
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
output.email; // ✓ string
output.password; // ✓ string
output.username; // ✗ TypeScript 错误
};Input vs Output Types
输入与输出类型
Schemas with transformations have different input and output types:
ts
const ProfileSchema = v.object({
age: v.pipe(
v.string(), // Input: string
v.transform((input) => Number(input)), // Output: number
v.number(),
),
birthDate: v.pipe(
v.string(), // Input: string
v.transform((input) => new Date(input)), // Output: Date
v.date(),
),
});
// In Field: field.input is string
// In onSubmit: output.age is number, output.birthDate is Date带有转换逻辑的Schema会有不同的输入和输出类型:
ts
const ProfileSchema = v.object({
age: v.pipe(
v.string(), // 输入类型: string
v.transform((input) => Number(input)), // 输出类型: number
v.number(),
),
birthDate: v.pipe(
v.string(), // 输入类型: string
v.transform((input) => new Date(input)), // 输出类型: Date
v.date(),
),
});
// 在Field中: field.input 类型为 string
// 在onSubmit中: output.age 类型为 number, output.birthDate 类型为 DateType-Safe Props
类型安全的属性
Pass forms to child components with proper typing:
tsx
import type { FormStore } from "@formisch/react";
type FormContentProps = {
of: FormStore<typeof LoginSchema>;
};
function FormContent({ of }: FormContentProps) {
return (
<Form of={of} onSubmit={(output) => console.log(output)}>
{/* ... */}
</Form>
);
}将表单传递给子组件时保持正确的类型:
tsx
import type { FormStore } from "@formisch/react";
type FormContentProps = {
of: FormStore<typeof LoginSchema>;
};
function FormContent({ of }: FormContentProps) {
return (
<Form of={of} onSubmit={(output) => console.log(output)}>
{/* ... */}
</Form>
);
}Generic Field Components
通用字段组件
Create reusable field components with proper typing:
tsx
import { useField, type FormStore } from "@formisch/react";
import * as v from "valibot";
type EmailInputProps = {
of: FormStore<v.GenericSchema<{ email: string }>>;
};
function EmailInput({ of }: EmailInputProps) {
const field = useField(of, { path: ["email"] });
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}创建带有正确类型的可复用字段组件:
tsx
import { useField, type FormStore } from "@formisch/react";
import * as v from "valibot";
type EmailInputProps = {
of: FormStore<v.GenericSchema<{ email: string }>>;
};
function EmailInput({ of }: EmailInputProps) {
const field = useField(of, { path: ["email"] });
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}Available Types
可用类型
ts
import type {
FormStore, // Form store type
FieldStore, // Field store type
FieldArrayStore, // Field array store type
SubmitHandler, // Submit handler function type
ValidPath, // Valid field path type
ValidArrayPath, // Valid array field path type
Schema, // Base schema type from Valibot
} from "@formisch/react";ts
import type {
FormStore, // 表单存储类型
FieldStore, // 字段存储类型
FieldArrayStore, // 字段数组存储类型
SubmitHandler, // 提交处理器函数类型
ValidPath, // 有效的字段路径类型
ValidArrayPath, // 有效的数组字段路径类型
Schema, // Valibot的基础Schema类型
} from "@formisch/react";Validation Timing
验证时机
validate Option
validate 选项
Controls when the first validation occurs:
| Value | Description |
|---|---|
| Validate immediately on form creation |
| Validate when field loses focus |
| Validate on every input change |
| Validate only on form submission (default) |
控制首次验证的触发时机:
| 值 | 描述 |
|---|---|
| 在表单创建后立即验证 |
| 当字段失焦时验证 |
| 在每次输入变化时验证 |
| 仅在表单提交时验证(默认值) |
revalidate Option
revalidate 选项
Controls when validation runs after the first validation:
| Value | Description |
|---|---|
| Revalidate when field loses focus |
| Revalidate on every input change (default) |
| Revalidate only on form submission |
控制首次验证之后的重验证触发时机:
| 值 | 描述 |
|---|---|
| 当字段失焦时重验证 |
| 在每次输入变化时重验证(默认值) |
| 仅在表单提交时重验证 |
Special Inputs
特殊输入组件
Select (Single)
单选下拉框
tsx
<Field of={form} path={["framework"]}>
{(field) => (
<select {...field.props}>
{options.map(({ label, value }) => (
<option key={value} value={value} selected={field.input === value}>
{label}
</option>
))}
</select>
)}
</Field>tsx
<Field of={form} path={["framework"]}>
{(field) => (
<select {...field.props}>
{options.map(({ label, value }) => (
<option key={value} value={value} selected={field.input === value}>
{label}
</option>
))}
</select>
)}
</Field>Select (Multiple)
多选下拉框
tsx
<Field of={form} path={["frameworks"]}>
{(field) => (
<select {...field.props} multiple>
{options.map(({ label, value }) => (
<option
key={value}
value={value}
selected={field.input?.includes(value)}
>
{label}
</option>
))}
</select>
)}
</Field>tsx
<Field of={form} path={["frameworks"]}>
{(field) => (
<select {...field.props} multiple>
{options.map(({ label, value }) => (
<option
key={value}
value={value}
selected={field.input?.includes(value)}
>
{label}
</option>
))}
</select>
)}
</Field>Checkbox
复选框
tsx
<Field of={form} path={["acceptTerms"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>tsx
<Field of={form} path={["acceptTerms"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>File Input
文件输入
File inputs cannot be controlled. Handle via UI around them:
tsx
<Field of={form} path={["avatar"]}>
{(field) => (
<div>
<input {...field.props} type="file" />
{field.input && <span>{field.input.name}</span>}
</div>
)}
</Field>文件输入无法被控制,可通过周边UI处理:
tsx
<Field of={form} path={["avatar"]}>
{(field) => (
<div>
<input {...field.props} type="file" />
{field.input && <span>{field.input.name}</span>}
</div>
)}
</Field>useField Hook
useField Hook
For complex field components, use the hook instead of the component:
useFieldFieldtsx
import { useField } from "@formisch/react";
function EmailInput({ form }) {
const field = useField(form, { path: ["email"] });
// Access field state in component logic
useEffect(() => {
if (field.errors) {
console.log("Email has errors:", field.errors);
}
}, [field.errors]);
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}When to use which:
- component — Multiple fields in the same component
Field - hook — Single field with component logic access
useField
对于复杂的字段组件,可使用 Hook替代组件:
useFieldFieldtsx
import { useField } from "@formisch/react";
function EmailInput({ form }) {
const field = useField(form, { path: ["email"] });
// 在组件逻辑中访问字段状态
useEffect(() => {
if (field.errors) {
console.log("邮箱存在错误:", field.errors);
}
}, [field.errors]);
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}使用场景对比:
- 组件 —— 在同一个组件中处理多个字段
Field - Hook —— 单个字段且需要访问组件逻辑
useField
Using Component Libraries
结合组件库使用
When using component libraries that don't expose their underlying native HTML elements, you cannot spread directly. Instead, use (React) or (other frameworks) to update the value programmatically:
field.propsfield.onChangefield.onInputtsx
import { DatePicker } from "some-component-library";
<Field of={form} path={["date"]}>
{(field) => (
<DatePicker
value={field.input}
onChange={(newDate) => field.onChange(newDate)}
/>
)}
</Field>;The method updates the field value and triggers validation, just like a native input would.
field.onChangeThis is useful for:
- Component libraries that wrap native elements without exposing them
- Complex custom inputs like date pickers, rich text editors, or color pickers
当使用不暴露底层原生HTML元素的组件库时,无法直接扩散。此时可使用 (React) 或 (其他框架) 以编程方式更新值:
field.propsfield.onChangefield.onInputtsx
import { DatePicker } from "some-component-library";
<Field of={form} path={["date"]}>
{(field) => (
<DatePicker
value={field.input}
onChange={(newDate) => field.onChange(newDate)}
/>
)}
</Field>;field.onChange这适用于:
- 组件库:封装了原生元素但未暴露的情况
- 复杂自定义输入:如日期选择器、富文本编辑器或颜色选择器
Async Submission
异步提交
tsx
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (values) => {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
// Set server-side errors
const data = await response.json();
setErrors(form, { path: ["email"], errors: [data.error] });
}
} catch (error) {
console.error("Submission failed:", error);
}
};tsx
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (values) => {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
// 设置服务端返回的错误
const data = await response.json();
setErrors(form, { path: ["email"], errors: [data.error] });
}
} catch (error) {
console.error("提交失败:", error);
}
};Common Patterns
常见模式
Loading State
加载状态
tsx
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? "Submitting..." : "Submit"}
</button>tsx
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? "提交中..." : "提交"}
</button>Submit on Enter
按回车提交
Formisch handles this automatically via the native element.
<form>Formisch通过原生元素自动处理此逻辑。
<form>Reset After Success
提交成功后重置
tsx
const handleSubmit: SubmitHandler<typeof Schema> = async (values) => {
await saveData(values);
// Full reset to initial state
reset(form);
// Or reset but keep current input values
reset(form, { keepInput: true });
};tsx
const handleSubmit: SubmitHandler<typeof Schema> = async (values) => {
await saveData(values);
// 完全重置为初始状态
reset(form);
// 或重置但保留当前输入值
reset(form, { keepInput: true });
};Server Data Sync
服务端数据同步
When server data changes, update the baseline without losing user edits:
tsx
// After refetching data from server
reset(form, {
initialInput: newServerData,
keepInput: true, // Keep user's current edits
keepTouched: true, // Keep touched state (optional)
});当服务端数据变化时,更新基准值但不丢失用户的编辑内容:
tsx
// 从服务端重新获取数据后
reset(form, {
initialInput: newServerData,
keepInput: true, // 保留用户当前的编辑内容
keepTouched: true, // 保留触碰状态(可选)
});Conditional Fields
条件字段
tsx
<Field of={form} path={["hasAccount"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>;
{
getInput(form, { path: ["hasAccount"] }) && (
<Field of={form} path={["accountId"]}>
{(field) => <input {...field.props} value={field.input} />}
</Field>
);
}tsx
<Field of={form} path={["hasAccount"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>;
{
getInput(form, { path: ["hasAccount"] }) && (
<Field of={form} path={["accountId"]}>
{(field) => <input {...field.props} value={field.input} />}
</Field>
);
}