You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:05:15