如何基于Zod自动为shadcn/ui表单必填字段添加星号标记
自动从Zod Schema识别必填字段并给shadcn/ui表单添加星号标记
核心思路
通过解析Zod Schema提取字段的必填状态,封装自定义的FormLabel组件自动判断并添加星号,无需手动传入required属性。以下是具体实现步骤:
步骤1:编写Zod Schema解析工具函数
先实现一个工具函数,遍历Zod Schema的字段,判断每个字段是否为必填(排除ZodOptional和ZodNullable类型),支持嵌套对象字段:
import { ZodSchema, ZodOptional, ZodNullable, z } from "zod"; // 判断字段是否为可选/可空类型 const isFieldOptional = (schema: any): boolean => { return schema instanceof ZodOptional || schema instanceof ZodNullable; }; // 递归提取Schema中所有字段的必填状态(支持嵌套对象) export const getRequiredFields = <T extends ZodSchema>(schema: T, parentKey = "") => { const shape = schema.shape; const requiredFields: Record<string, boolean> = {}; Object.keys(shape).forEach((key) => { const fieldSchema = shape[key]; const fullKey = parentKey ? `${parentKey}.${key}` : key; // 处理嵌套对象 if (fieldSchema instanceof z.ZodObject) { const nestedRequired = getRequiredFields(fieldSchema, fullKey); Object.assign(requiredFields, nestedRequired); } else { requiredFields[fullKey] = !isFieldOptional(fieldSchema); } }); return requiredFields; };
步骤2:创建表单必填状态上下文(可选,简化参数传递)
用React Context全局传递必填字段映射,避免每个FormLabel都手动传参:
import { createContext, useContext } from "react"; const FormRequiredContext = createContext<Record<string, boolean> | null>(null); export const FormRequiredProvider = FormRequiredContext.Provider; export const useFormRequired = () => { const context = useContext(FormRequiredContext); if (!context) { throw new Error("useFormRequired必须在FormRequiredProvider内部使用"); } return context; };
步骤3:封装自定义FormLabel组件
基于shadcn/ui的FormLabel二次封装,自动从上下文获取字段必填状态并添加星号:
import { FormLabel as ShadcnFormLabel, FormLabelProps } from "@/components/ui/form"; import { useFormRequired } from "./form-required-context"; interface CustomFormLabelProps extends FormLabelProps { fieldName: string; // 对应Zod Schema中的字段名(支持嵌套,如"contact.phone") } export const CustomFormLabel = ({ children, fieldName, ...props }: CustomFormLabelProps) => { const requiredFields = useFormRequired(); const isRequired = requiredFields[fieldName]; return ( <ShadcnFormLabel {...props}> {children} {isRequired && <span className="text-red-500 ml-1">*</span>} </ShadcnFormLabel> ); };
步骤4:在表单组件中使用
将解析后的必填字段映射通过上下文传递,直接使用自定义的CustomFormLabel:
import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { getRequiredFields } from "./utils"; import { FormRequiredProvider } from "./form-required-context"; import { CustomFormLabel } from "./custom-form-label"; // 你的Zod表单Schema const formSchema = z.object({ name: z.string().min(1, "姓名必填"), phone: z.string().min(11, "手机号格式错误"), website: z.string().url("网址格式错误").optional(), contact: z.object({ email: z.string().email().optional(), address: z.string().min(1, "地址必填"), }), }); type FormValues = z.infer<typeof formSchema>; export default function MyForm() { // 提取所有字段的必填状态 const requiredFields = getRequiredFields(formSchema); const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({ resolver: zodResolver(formSchema), }); const onSubmit = (data: FormValues) => { console.log(data); }; return ( <FormRequiredProvider value={requiredFields}> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 p-4"> {/* 姓名字段 */} <div className="space-y-2"> <CustomFormLabel htmlFor="name" fieldName="name">姓名</CustomFormLabel> <input {...register("name")} id="name" className="w-full px-3 py-2 border rounded-md" /> {errors.name && <p className="text-red-500 text-sm">{errors.name.message}</p>} </div> {/* 手机号字段 */} <div className="space-y-2"> <CustomFormLabel htmlFor="phone" fieldName="phone">手机号</CustomFormLabel> <input {...register("phone")} id="phone" className="w-full px-3 py-2 border rounded-md" /> {errors.phone && <p className="text-red-500 text-sm">{errors.phone.message}</p>} </div> {/* 网址字段(可选) */} <div className="space-y-2"> <CustomFormLabel htmlFor="website" fieldName="website">个人网址</CustomFormLabel> <input {...register("website")} id="website" className="w-full px-3 py-2 border rounded-md" /> {errors.website && <p className="text-red-500 text-sm">{errors.website.message}</p>} </div> {/* 嵌套地址字段(必填) */} <div className="space-y-2"> <CustomFormLabel htmlFor="contact.address" fieldName="contact.address">联系地址</CustomFormLabel> <input {...register("contact.address")} id="contact.address" className="w-full px-3 py-2 border rounded-md" /> {errors.contact?.address && <p className="text-red-500 text-sm">{errors.contact.address.message}</p>} </div> <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-md">提交</button> </form> </FormRequiredProvider> ); }
说明
- 无需手动给每个
FormLabel传required属性,所有必填状态自动从Zod Schema同步 - 支持嵌套对象字段,只需在
fieldName中传入完整路径(如contact.address) - 星号样式可根据项目需求自定义修改
内容的提问来源于stack exchange,提问作者Anish Shobith P S
相关产品推荐
相关产品推荐

