Home
Components

Zod Form

11/14/2025, 3:30:43 AM modified by Marvin

A 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

属性类型默认值描述
schemaZodObject<Record<string, ZodTypeAny>>-必需。Zod schema 对象,定义表单结构和验证规则
onSubmit(data: z.infer<T>) => void-必需。表单提交时的回调函数
defaultValuesPartial<z.infer<T>>{}表单的默认值
componentsTComponentMap{}自定义组件映射,用于覆盖默认的字段组件
classNamestring""表单容器的 CSS 类名
fieldClassNamestring""字段容器的 CSS 类名
childrenReact.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

PropertyTypeDefaultDescription
namestring-Required. The name of the field
fieldJsonSchemaTFieldJSONSchema-Required. The JSON Schema configuration of the field
componentsTComponentMap-Required. The component mapping
valueany-The value of the field
errorstring-The error message of the field
isRequiredbooleanfalseWhether 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
classNamestring-The CSS class name of the field container
属性类型默认值描述
namestring-必需。字段名称
fieldJsonSchemaTFieldJSONSchema-必需。字段的 JSON Schema 配置
componentsTComponentMap-必需。组件映射
valueany-字段值
errorstring-错误信息
isRequiredbooleanfalse是否为必填字段
updateField(name: string, value: any) => void-必需。更新字段值的函数
onValidate(name: string, value: any) => Promise<boolean>-必需。字段验证函数
classNamestring-字段容器的 CSS 类名

字段配置 (TFieldJSONSchema)

属性类型描述
componentstring指定使用的组件类型
typestring字段类型 (string, number, boolean 等)
labelstring字段标签
descriptionstring字段描述
placeholderstring占位符文本
defaultany默认值

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