formedible
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFormedible Skill
Formedible 使用指南
Formedible is a DECLARATIVE and SCHEMA-DRIVEN form library.
The schema is the single source of truth. Define forms via array configuration, NOT manual JSX.
fieldsUse this skill when working with Formedible forms - creating, debugging, or extending functionality.
Formedible 是一个声明式、基于Schema的表单库。
Schema是唯一的事实来源。通过数组配置来定义表单,而非手动编写JSX。
fields在创建、调试或扩展Formedible表单功能时,可参考本指南。
Quick Start
快速开始
⚠️ Formedible is DECLARATIVE and SCHEMA-DRIVEN. Do NOT write manual JSX for fields!
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
import { toast } from "sonner";
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
],
formOptions: {
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
toast.success("Form submitted!");
console.log(value);
},
},
});
return <Form className="space-y-4" />;
undefined⚠️ Formedible 是声明式、基于Schema的库。请勿手动为字段编写JSX!
tsx
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
import { toast } from "sonner";
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
],
formOptions: {
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
toast.success("Form submitted!");
console.log(value);
},
},
});
return <Form className="space-y-4" />;Field Types Quick Reference
字段类型速查
| Type | Key Config |
|---|---|
| Basic text input |
| Email validation |
| Password field |
| |
| |
| |
| |
| |
| |
| Boolean checkbox |
| Toggle switch |
| |
| |
| |
| 类型 | 关键配置 |
|---|---|
| 基础文本输入框 |
| 邮箱格式验证 |
| 密码输入框 |
| |
| |
| |
| |
| |
| |
| 布尔值复选框 |
| 开关切换组件 |
| |
| |
| |
Key Examples (Self-Contained)
核心示例(独立可运行)
Multi-Page Form with Dynamic Text
带动态文本的多页表单
tsx
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
import { toast } from "sonner";
const schema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
plan: z.enum(["basic", "pro"]),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "firstName", type: "text", label: "First Name", page: 1 },
{ name: "lastName", type: "text", label: "Last Name", page: 1 },
{
name: "email",
type: "email",
label: "Email",
page: 2,
description: "We'll contact {{firstName}} at {{email}}", // Dynamic text!
},
{
name: "plan",
type: "radio",
label: "Plan",
page: 2,
options: [
{ value: "basic", label: "Basic - Free" },
{ value: "pro", label: "Pro - $9/mo" },
],
},
],
pages: [
{ page: 1, title: "Personal Info", description: "Tell us about yourself" },
{ page: 2, title: "Contact", description: "How can we reach you, {{firstName}}?" },
],
progress: { showSteps: true, showPercentage: true },
formOptions: {
defaultValues: {
firstName: "",
lastName: "",
email: "",
plan: "basic" as const,
},
onSubmit: async ({ value }) => {
toast.success("Registered!");
},
},
});
return <Form className="space-y-4" />;tsx
import { useFormedible } from "@/hooks/use-formedible";
import { z } from "zod";
import { toast } from "sonner";
const schema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
plan: z.enum(["basic", "pro"]),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "firstName", type: "text", label: "First Name", page: 1 },
{ name: "lastName", type: "text", label: "Last Name", page: 1 },
{
name: "email",
type: "email",
label: "Email",
page: 2,
description: "We'll contact {{firstName}} at {{email}}", // Dynamic text!
},
{
name: "plan",
type: "radio",
label: "Plan",
page: 2,
options: [
{ value: "basic", label: "Basic - Free" },
{ value: "pro", label: "Pro - $9/mo" },
],
},
],
pages: [
{ page: 1, title: "Personal Info", description: "Tell us about yourself" },
{ page: 2, title: "Contact", description: "How can we reach you, {{firstName}}?" },
],
progress: { showSteps: true, showPercentage: true },
formOptions: {
defaultValues: {
firstName: "",
lastName: "",
email: "",
plan: "basic" as const,
},
onSubmit: async ({ value }) => {
toast.success("Registered!");
},
},
});
return <Form className="space-y-4" />;Conditional Fields AND Pages
条件字段与条件页面
tsx
const schema = z.object({
applicationType: z.enum(["individual", "business"]),
firstName: z.string().optional(),
companyName: z.string().optional(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "applicationType",
type: "radio",
label: "Application Type",
page: 1,
options: [
{ value: "individual", label: "Individual" },
{ value: "business", label: "Business" },
],
},
{
name: "firstName",
type: "text",
label: "First Name",
page: 2,
conditional: (values: any) => values.applicationType === "individual",
},
{
name: "companyName",
type: "text",
label: "Company Name",
page: 3,
conditional: (values: any) => values.applicationType === "business",
},
],
pages: [
{ page: 1, title: "Type" },
{
page: 2,
title: "Personal Info",
conditional: (values: any) => values.applicationType === "individual",
},
{
page: 3,
title: "Business Info",
conditional: (values: any) => values.applicationType === "business",
},
],
formOptions: {
defaultValues: {
applicationType: "individual" as const,
firstName: "",
companyName: "",
},
onSubmit: async ({ value }) => {
console.log(value);
},
},
});tsx
const schema = z.object({
applicationType: z.enum(["individual", "business"]),
firstName: z.string().optional(),
companyName: z.string().optional(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "applicationType",
type: "radio",
label: "Application Type",
page: 1,
options: [
{ value: "individual", label: "Individual" },
{ value: "business", label: "Business" },
],
},
{
name: "firstName",
type: "text",
label: "First Name",
page: 2,
conditional: (values: any) => values.applicationType === "individual",
},
{
name: "companyName",
type: "text",
label: "Company Name",
page: 3,
conditional: (values: any) => values.applicationType === "business",
},
],
pages: [
{ page: 1, title: "Type" },
{
page: 2,
title: "Personal Info",
conditional: (values: any) => values.applicationType === "individual",
},
{
page: 3,
title: "Business Info",
conditional: (values: any) => values.applicationType === "business",
},
],
formOptions: {
defaultValues: {
applicationType: "individual" as const,
firstName: "",
companyName: "",
},
onSubmit: async ({ value }) => {
console.log(value);
},
},
});Tabbed Form
标签页式表单
tsx
const schema = z.object({
firstName: z.string(),
theme: z.enum(["light", "dark"]),
notifications: z.boolean(),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "firstName", type: "text", label: "Name", tab: "personal" },
{
name: "theme",
type: "select",
label: "Theme",
tab: "preferences",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
],
},
{
name: "notifications",
type: "switch",
label: "Enable Notifications",
tab: "preferences",
},
],
tabs: [
{ id: "personal", label: "Personal Info", description: "About you" },
{ id: "preferences", label: "Preferences", description: "Settings" },
],
formOptions: {
defaultValues: {
firstName: "",
theme: "light" as const,
notifications: true,
},
onSubmit: async ({ value }) => console.log(value),
},
});tsx
const schema = z.object({
firstName: z.string(),
theme: z.enum(["light", "dark"]),
notifications: z.boolean(),
});
const { Form } = useFormedible({
schema,
fields: [
{ name: "firstName", type: "text", label: "Name", tab: "personal" },
{
name: "theme",
type: "select",
label: "Theme",
tab: "preferences",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
],
},
{
name: "notifications",
type: "switch",
label: "Enable Notifications",
tab: "preferences",
},
],
tabs: [
{ id: "personal", label: "Personal Info", description: "About you" },
{ id: "preferences", label: "Preferences", description: "Settings" },
],
formOptions: {
defaultValues: {
firstName: "",
theme: "light" as const,
notifications: true,
},
onSubmit: async ({ value }) => console.log(value),
},
});Dynamic Options (Dependent Fields)
动态选项(依赖字段)
tsx
const schema = z.object({
country: z.string(),
state: z.string(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "country",
type: "select",
label: "Country",
options: [
{ value: "us", label: "United States" },
{ value: "ca", label: "Canada" },
],
},
{
name: "state",
type: "select",
label: "State/Province",
options: (values: any) => {
if (values.country === "us") {
return [
{ value: "ca", label: "California" },
{ value: "ny", label: "New York" },
];
}
if (values.country === "ca") {
return [
{ value: "on", label: "Ontario" },
{ value: "qc", label: "Quebec" },
];
}
return [];
},
},
],
formOptions: {
defaultValues: { country: "", state: "" },
onSubmit: async ({ value }) => console.log(value),
},
});tsx
const schema = z.object({
country: z.string(),
state: z.string(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "country",
type: "select",
label: "Country",
options: [
{ value: "us", label: "United States" },
{ value: "ca", label: "Canada" },
],
},
{
name: "state",
type: "select",
label: "State/Province",
options: (values: any) => {
if (values.country === "us") {
return [
{ value: "ca", label: "California" },
{ value: "ny", label: "New York" },
];
}
if (values.country === "ca") {
return [
{ value: "on", label: "Ontario" },
{ value: "qc", label: "Quebec" },
];
}
return [];
},
},
],
formOptions: {
defaultValues: { country: "", state: "" },
onSubmit: async ({ value }) => console.log(value),
},
});Array Fields with Nested Objects
嵌套对象的数组字段
tsx
const schema = z.object({
teamMembers: z.array(
z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["dev", "design", "pm"]),
})
).min(1),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "teamMembers",
type: "array",
label: "Team Members",
section: {
title: "Team Composition",
description: "Add your team",
},
arrayConfig: {
itemType: "object",
itemLabel: "Team Member",
minItems: 1,
maxItems: 10,
sortable: true,
addButtonLabel: "Add Member",
defaultValue: {
name: "",
email: "",
role: "dev",
},
objectConfig: {
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
{
name: "role",
type: "select",
label: "Role",
options: [
{ value: "dev", label: "Developer" },
{ value: "design", label: "Designer" },
{ value: "pm", label: "Product Manager" },
],
},
],
},
},
},
],
formOptions: {
defaultValues: {
teamMembers: [{ name: "", email: "", role: "dev" as const }],
},
onSubmit: async ({ value }) => console.log(value),
},
});tsx
const schema = z.object({
teamMembers: z.array(
z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["dev", "design", "pm"]),
})
).min(1),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "teamMembers",
type: "array",
label: "Team Members",
section: {
title: "Team Composition",
description: "Add your team",
},
arrayConfig: {
itemType: "object",
itemLabel: "Team Member",
minItems: 1,
maxItems: 10,
sortable: true,
addButtonLabel: "Add Member",
defaultValue: {
name: "",
email: "",
role: "dev",
},
objectConfig: {
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
{
name: "role",
type: "select",
label: "Role",
options: [
{ value: "dev", label: "Developer" },
{ value: "design", label: "Designer" },
{ value: "pm", label: "Product Manager" },
],
},
],
},
},
},
],
formOptions: {
defaultValues: {
teamMembers: [{ name: "", email: "", role: "dev" as const }],
},
onSubmit: async ({ value }) => console.log(value),
},
});Analytics with Proper Memoization
带正确记忆化的分析功能
tsx
import React from "react";
const schema = z.object({
email: z.string().email(),
});
// MUST use useCallback for analytics callbacks!
const onFieldFocus = React.useCallback((fieldName: string, timestamp: number) => {
console.log(`Field "${fieldName}" focused at`, timestamp);
}, []);
const onFieldBlur = React.useCallback((fieldName: string, timeSpent: number) => {
console.log(`Field "${fieldName}" completed in ${timeSpent}ms`);
}, []);
const onFormComplete = React.useCallback((timeSpent: number, data: any) => {
console.log(`Form completed in ${timeSpent}ms`, data);
toast.success("Form completed!");
}, []);
// MUST useMemo the analytics config
const analyticsConfig = React.useMemo(
() => ({
onFieldFocus,
onFieldBlur,
onFormComplete,
}),
[onFieldFocus, onFieldBlur, onFormComplete]
);
const { Form } = useFormedible({
schema,
fields: [
{ name: "email", type: "email", label: "Email" },
],
analytics: analyticsConfig,
formOptions: {
defaultValues: { email: "" },
onSubmit: async ({ value }) => console.log(value),
},
});tsx
import React from "react";
const schema = z.object({
email: z.string().email(),
});
// 分析回调必须使用useCallback!
const onFieldFocus = React.useCallback((fieldName: string, timestamp: number) => {
console.log(`Field "${fieldName}" focused at`, timestamp);
}, []);
const onFieldBlur = React.useCallback((fieldName: string, timeSpent: number) => {
console.log(`Field "${fieldName}" completed in ${timeSpent}ms`);
}, []);
const onFormComplete = React.useCallback((timeSpent: number, data: any) => {
console.log(`Form completed in ${timeSpent}ms`, data);
toast.success("Form completed!");
}, []);
// 分析配置必须使用useMemo
const analyticsConfig = React.useMemo(
() => ({
onFieldFocus,
onFieldBlur,
onFormComplete,
}),
[onFieldFocus, onFieldBlur, onFormComplete]
);
const { Form } = useFormedible({
schema,
fields: [
{ name: "email", type: "email", label: "Email" },
],
analytics: analyticsConfig,
formOptions: {
defaultValues: { email: "" },
onSubmit: async ({ value }) => console.log(value),
},
});Rating Field with Config
带配置的评分字段
tsx
const schema = z.object({
satisfaction: z.number().min(1).max(5),
improvements: z.string().optional(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "satisfaction",
type: "rating",
label: "How satisfied are you?",
ratingConfig: {
max: 5,
allowHalf: false,
showValue: true,
},
},
{
name: "improvements",
type: "textarea",
label: "What can we improve?",
conditional: (values: any) => values.satisfaction < 4,
textareaConfig: {
rows: 4,
showWordCount: true,
maxLength: 500,
},
},
],
formOptions: {
defaultValues: { satisfaction: 5, improvements: "" },
onSubmit: async ({ value }) => console.log(value),
},
});tsx
const schema = z.object({
satisfaction: z.number().min(1).max(5),
improvements: z.string().optional(),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "satisfaction",
type: "rating",
label: "How satisfied are you?",
ratingConfig: {
max: 5,
allowHalf: false,
showValue: true,
},
},
{
name: "improvements",
type: "textarea",
label: "What can we improve?",
conditional: (values: any) => values.satisfaction < 4,
textareaConfig: {
rows: 4,
showWordCount: true,
maxLength: 500,
},
},
],
formOptions: {
defaultValues: { satisfaction: 5, improvements: "" },
onSubmit: async ({ value }) => console.log(value),
},
});Textarea with Configuration
带配置的文本域
tsx
const schema = z.object({
description: z.string().min(20).max(500),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "description",
type: "textarea",
label: "Description",
textareaConfig: {
rows: 4,
showWordCount: true,
maxLength: 500,
},
},
],
formOptions: {
defaultValues: { description: "" },
onSubmit: async ({ value }) => console.log(value),
},
});tsx
const schema = z.object({
description: z.string().min(20).max(500),
});
const { Form } = useFormedible({
schema,
fields: [
{
name: "description",
type: "textarea",
label: "Description",
textareaConfig: {
rows: 4,
showWordCount: true,
maxLength: 500,
},
},
],
formOptions: {
defaultValues: { description: "" },
onSubmit: async ({ value }) => console.log(value),
},
});Critical Patterns
核心开发模式
0. FORMEDIBLE IS DECLARATIVE AND SCHEMA DRIVEN (MOST IMPORTANT!)
0. Formedible 是声明式、基于Schema的库(最重要!)
tsx
// ❌ WRONG - Never write manual JSX for fields!
<form>
<input name="name" />
<input name="email" />
</form>
// ✅ CORRECT - Define fields declaratively via configuration!
const { Form } = useFormedible({
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
],
formOptions: {
onSubmit: async ({ value }) => console.log(value),
},
});THE SCHEMA IS THE SINGLE SOURCE OF TRUTH. Field names in MUST match schema keys exactly!
fieldstsx
// ❌ WRONG - Never write manual JSX for fields!
<form>
<input name="name" />
<input name="email" />
</form>
// ✅ CORRECT - Define fields declaratively via configuration!
const { Form } = useFormedible({
schema: z.object({
name: z.string(),
email: z.string().email(),
}),
fields: [
{ name: "name", type: "text", label: "Name" },
{ name: "email", type: "email", label: "Email" },
],
formOptions: {
onSubmit: async ({ value }) => console.log(value),
},
});Schema是唯一的事实来源。 中的字段名称必须与Schema的键完全匹配!
fields1. Always Use className on Form
1. 始终为Form组件添加className
tsx
<Form className="space-y-4" />tsx
<Form className="space-y-4" />2. Toast Notifications
2. 提示通知
tsx
import { toast } from "sonner";
onSubmit: async ({ value }) => {
toast.success("Success!", {
description: "Your data was saved",
});
}tsx
import { toast } from "sonner";
onSubmit: async ({ value }) => {
toast.success("Success!", {
description: "Your data was saved",
});
}3. Use as const
for Enums
as const3. 为枚举值使用as const
as consttsx
defaultValues: {
plan: "basic" as const, // ✅
role: "admin" as const, // ✅
}tsx
defaultValues: {
plan: "basic" as const, // ✅
role: "admin" as const, // ✅
}4. Dynamic Options = Function
4. 动态选项需使用函数形式
tsx
// ❌ Wrong
options: [{ value: "a", label: "A" }]
// ✅ Correct
options: (values) => {
if (values.category === "tech") return techOptions;
return [];
}tsx
// ❌ Wrong
options: [{ value: "a", label: "A" }]
// ✅ Correct
options: (values) => {
if (values.category === "tech") return techOptions;
return [];
}5. Conditional Returns Boolean
5. 条件判断需返回布尔值
tsx
// ❌ Wrong
conditional: (values) => {
if (values.type === "business") return true;
}
// ✅ Correct
conditional: (values) => values.type === "business"tsx
// ❌ Wrong
conditional: (values) => {
if (values.type === "business") return true;
}
// ✅ Correct
conditional: (values) => values.type === "business"6. Analytics Must Be Memoized
6. 分析功能必须使用记忆化
tsx
const callback = React.useCallback((...) => { ... }, []);
const analytics = React.useMemo(() => ({ callback }), [callback]);tsx
const callback = React.useCallback((...) => { ... }, []);
const analytics = React.useMemo(() => ({ callback }), [callback]);Build Workflow (CRITICAL!)
构建工作流(至关重要!)
PACKAGES ARE SOURCE OF TRUTH
- Edit:
packages/formedible/src/... - Build:
npm run build:pkg - Sync:
node scripts/quick-sync.js - Build web:
npm run build:web - Sync components:
npm run sync-components - Build web:
npm run build:web
NEVER edit web app files directly!
包文件是唯一事实来源
- 编辑:
packages/formedible/src/... - 构建:
npm run build:pkg - 同步:
node scripts/quick-sync.js - 构建Web应用:
npm run build:web - 同步组件:
npm run sync-components - 构建Web应用:
npm run build:web
切勿直接编辑Web应用文件!
Common Issues
常见问题
| Issue | Fix |
|---|---|
| Field not showing | Check field type in |
| Dynamic options not updating | Use function: |
| Validation not showing | Schema names must match field names exactly |
| Conditional always hidden | Return boolean, never undefined |
| Analytics not firing | Use |
| Pages not working | Pages start at 1, must be sequential |
| 问题 | 解决方法 |
|---|---|
| 字段不显示 | 检查 |
| 动态选项不更新 | 使用函数形式: |
| 验证不生效 | Schema中的名称必须与字段名称完全匹配 |
| 条件字段始终隐藏 | 返回布尔值,切勿返回undefined |
| 分析功能不触发 | 使用 |
| 多页功能失效 | 页码从1开始,且必须连续 |
Type Safety
类型安全
tsx
const schema = z.object({
name: z.string(),
age: z.number(),
});
type FormValues = z.infer<typeof schema>;
const { Form } = useFormedible<FormValues>({
schema,
formOptions: {
defaultValues: {
name: "", // Type-safe
age: 0, // Type-safe
},
},
});tsx
const schema = z.object({
name: z.string(),
age: z.number(),
});
type FormValues = z.infer<typeof schema>;
const { Form } = useFormedible<FormValues>({
schema,
formOptions: {
defaultValues: {
name: "", // Type-safe
age: 0, // Type-safe
},
},
});File Structure Reference
文件结构参考
packages/formedible/src/
├── hooks/use-formedible.tsx # Main hook
├── components/formedible/
│ ├── fields/ # All 22 field components
│ ├── layout/ # FormGrid, FormTabs, etc.
│ └── ui/ # Radix UI primitives
├── lib/formedible/
│ ├── types.ts # TypeScript interfaces
│ ├── field-registry.tsx # Field type mapping
│ └── template-interpolation.ts # Dynamic text resolutionpackages/formedible/src/
├── hooks/use-formedible.tsx # 核心Hook
├── components/formedible/
│ ├── fields/ # 所有22种字段组件
│ ├── layout/ # 表单网格、标签页等布局组件
│ └── ui/ # Radix UI 基础组件
├── lib/formedible/
│ ├── types.ts # TypeScript 接口定义
│ ├── field-registry.tsx # 字段类型映射表
│ └── template-interpolation.ts # 动态文本解析逻辑Adding New Field Types
添加新字段类型
- Create:
packages/formedible/src/components/formedible/fields/my-field.tsx - Use for consistency
BaseFieldWrapper - Add type to
packages/formedible/src/lib/formedible/types.ts - Register in
packages/formedible/src/lib/formedible/field-registry.tsx - Add to
packages/formedible/registry.json
See FIELD_TEMPLATES.md for templates.
- 创建文件:
packages/formedible/src/components/formedible/fields/my-field.tsx - 使用以保持组件一致性
BaseFieldWrapper - 在中添加类型定义
packages/formedible/src/lib/formedible/types.ts - 在中注册该字段
packages/formedible/src/lib/formedible/field-registry.tsx - 在中添加配置
packages/formedible/registry.json
请参考FIELD_TEMPLATES.md获取模板。