关于 formisch
formisch 是帮助 AI Agent 使用 Formisch 构建表单的 skill,面向多种框架的无头、基于 schema 的表单库。内容涵盖 Valibot 校验、表单状态和类型安全的字段操作。
主要功能
- 01基于 Valibot schema 校验
- 02管理表单与字段状态
- 03脏状态和错误处理工具
- 04动态字段数组
- 05TypeScript 类型推断
- 06八种框架的使用指南
适用场景
- 01在不同框架中实现校验表单
- 02跟踪编辑状态与初始值
- 03构建包含动态列表的表单
- 04在 React Native 中使用 Formisch
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ open-circle/ agent-skills --skill formisch安装后在 Claude Code 中输入/formisch确认已加载。
附带文件.claude-plugin/plugin.json
详细介绍
Formisch
此技能可帮助 AI Agent 高效地使用 Formisch,这是一款面向现代框架、基于 schema 的无头表单库。
何时使用此技能
- 用户询问如何使用 Formisch 处理表单时
- 管理表单状态和验证时
- 使用 Angular、Preact、Qwik、React、React Native、Solid、Svelte 或 Vue 表单时
- 将 Valibot schema 集成到表单中时
简介
Formisch 是一款基于 schema 的无头表单库,可用于多个框架。主要特点:
- 包体积小 — 起始大小约为 2.5 kB
- 基于 schema 的验证 — 使用 Valibot 进行类型安全的验证
- 无头设计 — 完全由你控制 UI
- 类型安全 — 完整支持 TypeScript 和自动补全
- 原生适配框架 — 在每个受支持的框架中提供原生性能
支持的框架
| 框架 | 包 | Hook/原语 |
|---|---|---|
| Angular | @formisch/angular | injectForm |
| Preact | @formisch/preact | useForm |
| Qwik | @formisch/qwik | useForm$ |
| React | @formisch/react | useForm |
| React Native | @formisch/react-native | useForm |
| SolidJS | @formisch/solid | createForm |
| Svelte | @formisch/svelte | createForm |
| Vue | @formisch/vue | useForm |
安装
1. 安装 Valibot(peer dependency)
npm install valibot
2. 为你的框架安装 Formisch
npm install @formisch/react # React
npm install @formisch/angular # Angular
npm install @formisch/vue # Vue
npm install @formisch/solid # SolidJS
npm install @formisch/preact # Preact
npm install @formisch/svelte # Svelte
npm install @formisch/qwik # Qwik
npm install @formisch/react-native # React Native
核心概念
Schema 优先设计
每个表单都始于一个 Valibot schema。类型会根据 schema 自动推断。
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."),
),
});
表单存储
表单存储管理所有表单状态。通过框架专属的 hook/原语访问它。
表单存储属性:
isSubmitting— 表单当前正在提交isSubmitted— 已尝试提交表单isValidating— 正在进行验证isTouched— 至少有一个字段已被触碰isEdited— 至少有一个字段已被编辑isDirty— 至少有一个字段的值与初始值不同isValid— 所有字段均通过验证errors— 根级验证错误
字段存储
每个字段都有自己的响应式存储,其中包含:
path— 指向该字段的路径数组input— 当前字段值errors— 字段专属错误isTouched— 字段已获得焦点isEdited— 字段值已被编辑isDirty— 字段值与初始值不同isValid— 字段通过验证props— 要展开到原生元素上的属性(Angular 则使用[formischControl]连接控件)onChange(React 和 React Native)/onInput(Solid、Svelte、Preact 和 Qwik)/setInput(Angular)— 以编程方式设置字段输入值。当字段无法连接到原生元素时使用。在 Vue 中,直接设置field.input(例如使用v-model)。
存储的响应性因框架而异。React、React Native、Solid、Svelte 和 Vue 会公开普通响应式属性。Angular 属性是信号,调用方式为 field.errors(),但 path 除外,它是普通值。Preact 和 Qwik 属性是信号;在条件判断和普通 TypeScript 逻辑中使用 .value。不要将一个框架的访问语法照搬到另一个框架中。
Dirty 状态跟踪
Formisch 会跟踪每个字段的两个输入值:
- 初始输入 — dirty 状态跟踪的基准值(服务端状态)
- 当前输入 — 用户正在编辑的值(客户端状态)
当当前输入与初始输入不同时,isDirty 会变为 true。
框架示例
Angular 示例
Angular 使用信号、依赖注入和指令,而不是 JSX 组件 API。
import { Component } from "@angular/core";
import {
FormischControl,
FormischField,
FormischForm,
injectForm,
type SubmitHandler,
} from "@formisch/angular";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
@Component({
selector: "app-login",
imports: [FormischForm, FormischField, FormischControl],
template: `
<form [formischForm]="loginForm" [formischSubmit]="handleSubmit">
<ng-container *formischField="['email'] of loginForm; let field">
<input [formischControl]="field" type="email" />
@if (field.errors(); as errors) {
<div>{{ errors[0] }}</div>
}
</ng-container>
<button type="submit" [disabled]="loginForm.isSubmitting()">Login</button>
</form>
`,
})
export class LoginComponent {
readonly loginForm = injectForm({ schema: LoginSchema });
readonly handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
}
让 [formischControl] 同步原生控件。除非 value 用于标识单选按钮组或复选框组中的选项,否则不要添加会产生冲突的 [value] 或 [checked] 绑定。
React Native 示例
React Native 没有 DOM <form> 元素或 Formisch Form 组件。使用 handleSubmit,并将 field.props 绑定到 TextInput。
import { Field, handleSubmit, useForm } from "@formisch/react-native";
import { Button, TextInput, View } from "react-native";
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 LoginScreen() {
const loginForm = useForm({ schema: LoginSchema });
const submitForm = handleSubmit(loginForm, (output) => console.log(output));
return (
<View>
<Field of={loginForm} path={["email"]}>
{(field) => (
<TextInput
{...field.props}
value={field.input}
autoCapitalize="none"
keyboardType="email-address"
/>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<TextInput {...field.props} value={field.input} secureTextEntry />
)}
</Field>
<Button title="Login" onPress={submitForm} />
</View>
);
}
React Native 文本输入框是受控的,因此始终传入 value={field.input}。对于开关、滑块、选择器和非字符串值,请使用 field.onChange(value)。
React 示例
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>
);
}
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-model="field.input" v-bind="field.props" type="email" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<Field :of="loginForm" :path="['password']" v-slot="field">
<div>
<input v-model="field.input" v-bind="field.props" type="password" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<button type="submit">Login</button>
</Form>
</template>
SolidJS 示例
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>
);
}
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>
Qwik 示例
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>
);
});
表单配置
const form = useForm({
// Required: Valibot schema
schema: MySchema,
// Optional: Initial values (partial allowed)
initialInput: {
email: "[email protected]",
},
// Optional: Empty values for required fields without initial input
// Required strings default to ''; number, boolean, and date to undefined
emptyInput: {
number: 0,
},
// Optional: When first validation occurs
// Options: 'initial' | 'touch' | 'input' | 'change' | 'blur' | 'submit' (default)
validate: "submit",
// Optional: When a field is validated again once it already has an
// error or the form has been submitted
// Options: 'touch' | 'input' (default) | 'change' | 'blur' | 'submit'
revalidate: "input",
});
在 Qwik 中,useForm$ 必须接收一个返回配置的函数,例如 useForm$(() => ({ schema: MySchema }))。这样 Qwik 才能将配置转换为 QRL。
可选字段和可空字段仍为 undefined。emptyInput 仅为输入值为 undefined 的必填字段提供回退值。
字段路径
路径是类型安全的数组,用于引用架构中的字段。
// 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} />
))}
表单方法
所有方法都遵循一致的 API 模式:
- 第一个参数:表单 store
- 第二个参数:配置对象
读取值
import {
getDeepError,
getDeepErrorEntries,
getDeepErrorEntry,
getDeepErrors,
getErrors,
getInput,
} 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 (including form-level errors)
const allErrors = getDeepErrors(form);
// Get all errors of a field and its descendants
const todoErrors = getDeepErrors(form, { path: ["todos"] });
// Get every error together with its field path
const errorEntries = getDeepErrorEntries(form);
// Get only the first error of a field and its descendants
const firstTodoError = getDeepError(form, { path: ["todos"] });
// Get only the first error together with its field path
const firstErrorEntry = getDeepErrorEntry(form);
表单级别的 form.errors 和 getErrors(form) 仅包含根级错误。需要获取后代字段错误时,请使用深层错误方法。单数形式的 getDeepError 和 getDeepErrorEntry 会在找到第一个有错误的字段时停止,这适用于为嵌套结构显示单条消息。
读取脏状态
import {
getDirtyInput,
getDirtyPaths,
isDirty,
pickDirty,
} from "@formisch/react";
// Raw dirty form input, or undefined when nothing is dirty
const dirtyInput = getDirtyInput(form);
// Paths of dirty fields (arrays are treated as atomic values)
const dirtyPaths = getDirtyPaths(form);
// Cheap boolean check when the dirty values are not needed
const hasChanges = isDirty(form);
// Boolean check scoped to a field and its descendants
const emailChanged = isDirty(form, { path: ["email"] });
// In a submit handler, keep transformed output only where fields are dirty
const dirtyOutput = pickDirty(form, { from: output });
getDirtyInput 返回原始表单输入。pickDirty 会将表单的脏值掩码应用于给定值,适用于已验证和转换后的提交输出。
同级方法 isTouched、isEdited 和 isValid 遵循与 isDirty 相同的模式。不带配置调用时,它们会检查整个表单;带 path 调用时,则会检查特定字段及其后代字段。
设置值
import { setInput, setErrors, reset } from "@formisch/react";
// Set field value (updates current input, not initial)
setInput(form, { path: ["email"], input: "[email protected]" });
// 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 a single field
reset(form, { path: ["email"] });
// Reset with new initial values
reset(form, {
initialInput: { email: "", password: "" },
});
// Reset but keep current input
reset(form, {
initialInput: newServerData,
keepInput: true,
});
reset 还接受 keepTouched、keepEdited 和 keepErrors 标志。表单级别的重置还接受 keepSubmitted。所有标志默认均为 false。
表单控制
import { validate, focus, submit, handleSubmit } from "@formisch/react";
// Validate form manually (returns a Promise of a Valibot SafeParseResult)
const result = await validate(form);
if (result.success) {
console.log(result.output);
} else {
console.log(result.issues);
}
// 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);
});
submit 需要已注册的 DOM 表单,且不会由 @formisch/react-native 导出。在 React Native 中以及没有 <form> 元素的布局中,请改为调用 handleSubmit 返回的函数。
字段数组
对于动态字段列表,请使用 FieldArray 和数组操作方法。
字段数组 store 会公开 path、items(用作键的稳定项目 ID)、errors、isTouched、isEdited、isDirty 和 isValid。
架构
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 示例
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>
);
}
数组方法
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" },
});
TypeScript 集成
类型推断
类型会根据 Valibot 架构自动推断:
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
};
输入类型与输出类型
带有转换的架构具有不同的输入类型和输出类型:
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
类型安全的 Props
以适当的类型将表单传递给子组件:
import { Form, type FormStore, useForm } from "@formisch/react";
export default function LoginPage() {
const loginForm = useForm({ schema: LoginSchema });
return <FormContent of={loginForm} />;
}
type FormContentProps = {
of: FormStore<typeof LoginSchema>;
};
function FormContent({ of }: FormContentProps) {
return (
<Form of={of} onSubmit={(output) => console.log(output)}>
{/* ... */}
</Form>
);
}
泛型字段组件
创建具有适当类型的可复用字段组件:
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>
);
}
v.GenericSchema<{ email: string }> 约束接受架构中包含 email 字段且类型为 string 的任何表单。TypeScript 会在编译时捕获类型不匹配。
可用类型
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";
验证时机
validate 选项
控制首次验证的时机:
| 值 | 描述 |
|---|---|
'initial' | 创建表单时立即验证 |
'touch' | 字段首次获得焦点时验证 |
'input' | 每次输入事件时验证 |
'change' | 值提交时验证(值已确认) |
'blur' | 字段失去焦点时验证 |
'submit' | 仅在提交表单时验证(默认) |
revalidate 选项
控制字段在已有错误或表单已提交后,再次验证的时机:
| 值 | 描述 |
|---|---|
'touch' | 字段首次获得焦点时重新验证 |
'input' | 每次输入事件时重新验证(默认) |
'change' | 值提交时,在 change 事件时重新验证 |
'blur' | 字段失去焦点时重新验证 |
'submit' | 仅在提交表单时重新验证 |
特殊输入
Select(单选)
<Field of={form} path={["framework"]}>
{(field) => (
<select {...field.props} value={field.input ?? ""}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
)}
</Field>
Select(多选)
<Field of={form} path={["frameworks"]}>
{(field) => (
<select {...field.props} value={field.input ?? []} multiple>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
)}
</Field>
复选框
<Field of={form} path={["acceptTerms"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>
文件输入框
文件输入框无法受控。请通过其周围的 UI 进行处理:
<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,而不是 Field 组件:
import { useField } from "@formisch/react";
import { useEffect } from "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>
);
}
如何选择:
Field组件 — 在同一组件中使用多个字段useFieldhook — 需要访问组件逻辑的单个字段
useFieldArray hook 是 FieldArray 组件的对应版本。在 Angular 中,请使用 injectField 和 injectFieldArray 函数,或 *formischField 和 *formischFieldArray 指令。
使用组件库
使用不公开底层原生元素的组件库时,不能直接展开 field.props。请改用 field.onChange(React 和 React Native)、field.onInput(Solid、Svelte、Preact 和 Qwik)、field.setInput(Angular)以编程方式更新值,或直接赋值给 field.input(Vue):
import { DatePicker } from "some-component-library";
<Field of={form} path={["date"]}>
{(field) => (
<DatePicker
value={field.input}
onChange={(newDate) => field.onChange(newDate)}
/>
)}
</Field>;
这些 setter 会更新字段值并触发验证,效果与原生输入框相同。
适用于:
- 组件库:封装原生元素但不公开这些元素
- 复杂的自定义输入框,例如日期选择器、富文本编辑器或颜色选择器
异步提交
import { setErrors, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
const loginForm = useForm({ schema: LoginSchema });
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (output) => {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(output),
});
if (!response.ok) {
// Set server-side errors
const data = await response.json();
setErrors(loginForm, { path: ["email"], errors: [data.error] });
}
} catch (error) {
console.error("Submission failed:", error);
}
};
异步处理程序解析之前,表单的 isSubmitting 状态会一直保持为 true。如果处理程序抛出错误,Formisch 会捕获该错误,并将其消息作为表单级错误设置到 form.errors 中。
常见模式
加载状态
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? "Submitting..." : "Submit"}
</button>
按 Enter 提交
Formisch 会通过原生 <form> 元素自动处理此操作。
成功后重置
const handleSubmit: SubmitHandler<typeof Schema> = async (output) => {
await saveData(output);
// Full reset to initial state
reset(form);
// Or reset but keep current input values
reset(form, { keepInput: true });
};
同步服务器数据
服务器数据发生变化时,更新基线而不丢失用户的编辑内容:
// After refetching data from server
reset(form, {
initialInput: newServerData,
keepInput: true, // Keep user's current edits
keepTouched: true, // Keep touched state (optional)
});
条件字段
<Form of={form} onSubmit={handleSubmit}>
<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>
)}
</Form>
在 React 中,在渲染期间调用 getInput 具有响应性,因为 useForm 和 useField 会在调用它们的组件中启用信号追踪。在其他框架中,读取操作由各自的响应式作用域进行追踪。
更多资源
- Formisch 文档
- Formisch 编码 Agent 指南
- Formisch MCP 服务:
https://formisch.dev/mcp(search_docs、get_doc和list_docs) - 在任何文档 URL 后追加
.md,即可获取适用于 Agent 的 Markdown;也可以使用https://formisch.dev/llms-{framework}.txt获取特定框架的索引 - Formisch GitHub
- Valibot 文档