Components
Zod Form
11/14/2025, 3:30:43 AM modified by MarvinA form component that uses Zod for validation.
Install
Loading...
Preview
'use client';import type React from 'react';import { useCallback, useMemo, useState } from 'react';import { z } from 'zod';import type { TComponentMap } from './default-components';import { ZodField, type ZodFieldProps } from './zod-field';type ZodSchema = z.ZodObject<Record<string, z.ZodTypeAny>>;interface ZodFormProps<T extends ZodSchema> { schema: T; onSubmit: (data: z.infer<T>) => void; defaultValues?: Partial<z.infer<T>>; components?: TComponentMap; className?: string; fieldClassName?: string; children?: React.ReactNode; renderFields?: (props: ZodFieldProps) => React.ReactNode;}export function ZodForm<T extends ZodSchema>(props: ZodFormProps<T>) { const { schema, onSubmit, defaultValues = {}, className = '', components = {}, fieldClassName = '', renderFields, children, } = props; // 使用 Zod v4 内置的 JSON Schema 转换 const jsonSchema = useMemo(() => z.toJSONSchema(schema), [schema]); // 初始化表单数据 const initialFormData = useMemo(() => { const schemaDefaults = extractDefaultValues(jsonSchema); return { ...schemaDefaults, ...defaultValues }; }, [jsonSchema, defaultValues]); const [formData, setFormData] = useState<any>(initialFormData); const [errors, setErrors] = useState<Record<string, string>>({}); // 更新字段值 const updateField = (name: string, value: any) => { setFormData((prev: any) => ({ ...prev, [name]: value })); // 清除该字段的错误 if (errors[name]) { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[name]; return newErrors; }); } }; const errorHandler = useCallback( (args: { error: NonNullable<z.ZodSafeParseResult<unknown>['error']>; name?: string; bypassCallback?: (data: any) => void; }) => { const { name, error } = args; const newErrors: Record<string, string> = {}; error.issues.forEach((issue) => { const path = name || issue.path.join('.'); newErrors[path] = issue.message; }); setErrors(newErrors); }, [], ); const onValidate = async (name: string, value: any) => { const fieldSchema = schema.shape[name as keyof typeof schema.shape]; if (!fieldSchema) { return true; } const result = fieldSchema.safeParse(value); if (result.success) { setErrors({}); } else { errorHandler({ error: result.error, name }); } return result.success; }; // 表单提交 const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const result = schema.safeParse(formData); if (result.success) { setErrors({}); onSubmit(result.data); } else { errorHandler({ error: result.error, }); } }; const handleReset = () => { setFormData(initialFormData); setErrors({}); }; const fields = useMemo(() => { return Object.entries(jsonSchema.properties || {}); }, [jsonSchema]); return ( <form onReset={handleReset} onSubmit={handleSubmit} className={`${className}`}> {fields.map(([name, fieldJsonSchema]) => { const props = { name, fieldJsonSchema, components, value: formData[name], error: errors[name], updateField, isRequired: jsonSchema.required?.includes(name), onValidate, className: fieldClassName, }; return renderFields ? renderFields(props) : <ZodField key={name} {...props} />; })} {children} </form> );}function extractDefaultValues(jsonSchema: any): Record<string, any> { const defaults: Record<string, any> = {}; if (jsonSchema.properties) { for (const [key, field] of Object.entries<any>(jsonSchema.properties)) { if (field.default !== undefined) { defaults[key] = field.default; } } } return defaults;}import type { INativeInputProps, TFieldJSONSchema } from './default-components';import { defaultComponents, type TComponentMap } from './default-components';export type ZodFieldProps<T = string> = INativeInputProps<T> & { components: TComponentMap; isRequired?: boolean; name: string; updateField: (name: string, value: T) => void; className?: string; onValidate: (name: string, value: T) => void;};export function ZodField({ name, fieldJsonSchema, components, isRequired, value, error, updateField, onValidate,}: ZodFieldProps) { // 根据类型渲染对应的组件 const { component: FieldComponent } = extractComponent({ fieldJsonSchema, components, }); if (!FieldComponent) return null; const { label, description } = fieldJsonSchema; return ( <FieldComponent name={name} label={label || name} description={description} value={value} error={error} isRequired={isRequired} onValidate={onValidate} onChange={(newValue) => { updateField(name, newValue); }} fieldJsonSchema={fieldJsonSchema} /> );}type ExtractComponentParams<T> = (params: { fieldJsonSchema: TFieldJSONSchema; components: T }) => { component: T[keyof T];};const extractComponent: ExtractComponentParams<TComponentMap> = (params) => { const { fieldJsonSchema } = params; const components = Object.assign(defaultComponents, params.components); const component = fieldJsonSchema.component || fieldJsonSchema.type; if (component && components[component]) { return { component: components[component] }; } return { component: () => ( <div> <mark> Unsupported type: <strong>{component}</strong>, Please use <strong> defineComponents</strong> to define a custom component. </mark> </div> ), };};'use client';import { z } from 'zod';import { ZodFormFooter, ZodFormReset, ZodFormSubmit } from './default-components';import { ZodForm } from './zod-form';const schema = z.object({ name: z.string().min(1, '姓名不能为空'), email: z.email('请输入有效的邮箱地址'), age: z.number().min(18, '年龄必须大于18岁'),});export const App = () => { return ( <div> <ZodForm schema={schema} onSubmit={console.log}> <ZodFormFooter> <ZodFormSubmit>Submit</ZodFormSubmit> <ZodFormReset variant={'destructive'}>Reset</ZodFormReset> </ZodFormFooter> </ZodForm> </div> );};import { type FC, type InputHTMLAttributes, useMemo } from 'react';import { Button } from '@/components/ui/button';import { Input } from '@/components/ui/input';import { cn } from '@/lib/utils';export type TFieldJSONSchema = { component?: string; placeholder?: string; label?: string; description?: string; type?: string; [key: string]: any;};export type INativeInputProps<T> = Pick< InputHTMLAttributes<HTMLInputElement>, 'placeholder' | 'disabled' | 'required' | 'className'> & { onChange?: (value: T) => void; onValidate?: (name: string, value: T) => Promise<boolean>; readonly value?: T; error?: string; label?: string; name: string; description?: string; options?: T[]; isRequired?: boolean; fieldJsonSchema?: any;};export type NativeComponent<T> = React.ComponentType<INativeInputProps<T>>;// ============ 默认组件 ============export const NativeInput: FC<INativeInputProps<string | number>> = ({ value, onChange, name, fieldJsonSchema, onValidate, isRequired, error,}) => { const isNumberInput = useMemo(() => fieldJsonSchema.type === 'number', [fieldJsonSchema.type]); const label: string = fieldJsonSchema.label || name; return ( <div> <label htmlFor={name}> {label}: {isRequired && <span>*</span>} </label> <Input type={isNumberInput ? 'number' : 'text'} name={name} value={value ?? ''} onChange={(e) => { const newValue = isNumberInput ? Number(e.target.value) : e.target.value; onChange?.(newValue); onValidate?.(name, newValue); }} className="native-input" /> {error && <p>{error}</p>} </div> );};export type TComponentType = 'string' | 'checkbox' | 'radio' | 'select';export type TComponentMap<T = any> = Record<string, NativeComponent<T>> & Partial<Record<TComponentType, NativeComponent<T>>>;export function defineComponents<T extends TComponentMap>(components: T) { return components as T;}export const defaultComponents = defineComponents({ // 类型级别的映射 string: NativeInput, number: NativeInput,});export type ButtonProps = React.ComponentProps<typeof Button>;export function ZodFormSubmit(props: ButtonProps) { return <Button type="submit" {...props} />;}export function ZodFormReset(props: ButtonProps) { return <Button type="reset" {...props} />;}export function ZodFormFooter(props: { className?: string; children: React.ReactNode }) { const { className, children } = props; return <div className={cn('flex justify-end gap-2', className)}>{children}</div>;}API Reference
ZodForm
主要的表单组件,基于 Zod schema 自动生成表单字段。
Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
schema | ZodObject<Record<string, ZodTypeAny>> | - | 必需。Zod schema 对象,定义表单结构和验证规则 |
onSubmit | (data: z.infer<T>) => void | - | 必需。表单提交时的回调函数 |
defaultValues | Partial<z.infer<T>> | {} | 表单的默认值 |
components | TComponentMap | {} | 自定义组件映射,用于覆盖默认的字段组件 |
className | string | "" | 表单容器的 CSS 类名 |
fieldClassName | string | "" | 字段容器的 CSS 类名 |
children | React.ReactNode | - | 表单子元素,通常用于添加提交按钮等 |
renderFields | (props: ZodFieldProps) => React.ReactNode | - | 自定义字段渲染函数 |
Example
import {
ZodForm,
ZodFormFooter,
ZodFormSubmit,
ZodFormReset,
} from "@/registry/new-york/blocks/zod-form";
import { z } from "zod";
const userSchema = z
.object({
username: z.string().min(3, "用户名至少3个字符"),
email: z.string().email("请输入有效邮箱"),
password: z.string().min(8, "密码至少8个字符"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "密码不匹配",
path: ["confirmPassword"],
});
function UserRegistrationForm() {
const handleSubmit = (data) => {
console.log("用户数据:", data);
// 处理注册逻辑
};
return (
<ZodForm
schema={userSchema}
onSubmit={handleSubmit}
defaultValues={{
username: "",
email: "",
password: "",
confirmPassword: "",
}}
className="max-w-md mx-auto"
>
<ZodFormFooter>
<ZodFormSubmit>注册</ZodFormSubmit>
<ZodFormReset variant="outline">重置</ZodFormReset>
</ZodFormFooter>
</ZodForm>
);
}ZodField
A single form field component that renders specific input controls.
Props
| Property | Type | Default | Description |
|---|---|---|---|
name | string | - | Required. The name of the field |
fieldJsonSchema | TFieldJSONSchema | - | Required. The JSON Schema configuration of the field |
components | TComponentMap | - | Required. The component mapping |
value | any | - | The value of the field |
error | string | - | The error message of the field |
isRequired | boolean | false | Whether the field is required |
updateField | (name: string, value: any) => void | - | Required. The function to update the field value |
onValidate | (name: string, value: any) => Promise<boolean> | - | Required. The function to validate the field |
className | string | - | The CSS class name of the field container |
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
name | string | - | 必需。字段名称 |
fieldJsonSchema | TFieldJSONSchema | - | 必需。字段的 JSON Schema 配置 |
components | TComponentMap | - | 必需。组件映射 |
value | any | - | 字段值 |
error | string | - | 错误信息 |
isRequired | boolean | false | 是否为必填字段 |
updateField | (name: string, value: any) => void | - | 必需。更新字段值的函数 |
onValidate | (name: string, value: any) => Promise<boolean> | - | 必需。字段验证函数 |
className | string | - | 字段容器的 CSS 类名 |
字段配置 (TFieldJSONSchema)
| 属性 | 类型 | 描述 |
|---|---|---|
component | string | 指定使用的组件类型 |
type | string | 字段类型 (string, number, boolean 等) |
label | string | 字段标签 |
description | string | 字段描述 |
placeholder | string | 占位符文本 |
default | any | 默认值 |
Helper Components
ZodFormSubmit
表单提交按钮组件。
<ZodFormSubmit>提交</ZodFormSubmit>
<ZodFormSubmit variant="destructive" size="lg">删除</ZodFormSubmit>ZodFormReset
表单重置按钮组件。
<ZodFormReset>重置</ZodFormReset>
<ZodFormReset variant="outline">清空</ZodFormReset>ZodFormFooter
表单底部容器组件,用于包装提交和重置按钮。
<ZodFormFooter className="flex justify-between">
<ZodFormReset>重置</ZodFormReset>
<ZodFormSubmit>提交</ZodFormSubmit>
</ZodFormFooter>Git Commit History(1 commits)
feat: build
Marvin
11月14日 03:30
e50053c2