当前位置: 首页 > news >正文

react动态表单

来个例子

比如有两种登录方式

// 导入表单验证库
import * as z from 'zod';
// 导入消息提示组件
import { toast } from 'sonner';
import { useState } from 'react';
// 导入 UI 组件
import { Input } from '@repo/shadcn-comps/input';
import { Button } from '@repo/shadcn-comps/button';
import { Switch } from '@repo/shadcn-comps/switch';
// 导入表单处理相关
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import { Field, FieldError, FieldGroup, FieldLabel } from '@repo/shadcn-comps/field';// 使用 discriminatedUnion 定义不同登录方式的验证规则
const loginSchema = z.discriminatedUnion('type', [z.object({type: z.literal('account'),account: z.string().min(5, '账号至少需要5个字符').max(32, '账号最多32个字符'),password: z.string().min(6, '密码至少需要6个字符').max(30, '密码最多30个字符'),}),z.object({type: z.literal('email'),email: z.string().email('请输入有效的邮箱地址'),code: z.string().length(6, '验证码必须为6位'),}),
]);type LoginFormData = z.infer<typeof loginSchema>;/*** 登录表单组件* 支持账号密码登录和邮箱验证码登录两种方式*/
export default function LoginForm() {const [loginType, setLoginType] = useState<'account' | 'email'>('account');// 初始化表单,配置验证器和默认值const form = useForm<LoginFormData>({resolver: zodResolver(loginSchema),defaultValues: {type: 'account',account: '',password: '',},});// 切换登录方式时重置表单const handleLoginTypeChange = () => {const newType = loginType === 'account' ? 'email' : 'account';setLoginType(newType);// 重置表单并设置新的默认值if (newType === 'account') {form.reset({type: 'account',account: '',password: '',} );} else {form.reset({type: 'email',email: '',code: '',} );}};// 表单提交处理函数function onSubmit(data: LoginFormData) {toast.info(JSON.stringify(data, null, 2));}return (<div className="p-2 w-100"><div><Switch checked={loginType === 'email'} onCheckedChange={handleLoginTypeChange} /></div>{/* 表单主体 */}<form id="login-form" onSubmit={form.handleSubmit(onSubmit)}>{loginType === 'email' ? (<FieldGroup>{/* 邮箱字段 */}<Controllername="email"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-email">邮箱</FieldLabel><Input {...field} id="login-email" aria-invalid={fieldState.invalid} placeholder="请输入邮箱" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/>{/* 验证码字段 */}<Controllername="code"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-code">验证码</FieldLabel><Input {...field} id="login-code" aria-invalid={fieldState.invalid} placeholder="请输入6位验证码" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/></FieldGroup>) : (<FieldGroup>{/* 账号字段 */}<Controllername="account"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-account">账号</FieldLabel><Input {...field} id="login-account" aria-invalid={fieldState.invalid} placeholder="请输入账号" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/>{/* 密码字段 */}<Controllername="password"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-password">密码</FieldLabel><Input{...field}id="login-password"type="password"aria-invalid={fieldState.invalid}placeholder="请输入密码"/>{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/></FieldGroup>)}</form>{/* 操作按钮组 */}<Field orientation="horizontal" className="mt-2"><Button type="button" variant="outline" onClick={() => form.reset()}>重置</Button><Button type="submit" form="login-form">提交</Button></Field></div>);
}

假设说无论哪种登录方式,我们都有一个字段 叫做登录感言,字符串类型 20 个字以内,我们可以这样使用 Zod 的 .and() 方法来合并公共字段和特定字段:

// 导入表单验证库
import * as z from 'zod';
// 导入消息提示组件
import { toast } from 'sonner';
import { useState } from 'react';
// 导入 UI 组件
import { Input } from '@repo/shadcn-comps/input';
import { Button } from '@repo/shadcn-comps/button';
import { Switch } from '@repo/shadcn-comps/switch';
// 导入表单处理相关
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import { Field, FieldError, FieldGroup, FieldLabel } from '@repo/shadcn-comps/field';// 定义公共字段
const commonFields = z.object({loginMessage: z.string().max(20, '登录感言最多20个字').optional(),
});// 使用 discriminatedUnion 定义不同登录方式的验证规则,并与公共字段合并
const loginSchema = commonFields.and(z.discriminatedUnion('type', [z.object({type: z.literal('account'),account: z.string().min(5, '账号至少需要5个字符').max(32, '账号最多32个字符'),password: z.string().min(6, '密码至少需要6个字符').max(30, '密码最多30个字符'),}),z.object({type: z.literal('email'),email: z.string().email('请输入有效的邮箱地址'),code: z.string().length(6, '验证码必须为6位'),}),])
);type LoginFormData = z.infer<typeof loginSchema>;/*** 登录表单组件* 支持账号密码登录和邮箱验证码登录两种方式*/
export default function LoginForm() {const [loginType, setLoginType] = useState<'account' | 'email'>('account');// 初始化表单,配置验证器和默认值const form = useForm<LoginFormData>({resolver: zodResolver(loginSchema),defaultValues: {type: 'account',account: '',password: '',loginMessage: '',} });// 切换登录方式时重置表单const handleLoginTypeChange = () => {const newType = loginType === 'account' ? 'email' : 'account';setLoginType(newType);// 重置表单并设置新的默认值if (newType === 'account') {form.reset({type: 'account',account: '',password: '',loginMessage: '',} );} else {form.reset({type: 'email',email: '',code: '',loginMessage: '',} );}};// 表单提交处理函数function onSubmit(data: LoginFormData) {toast.info(JSON.stringify(data, null, 2));}return (<div className="p-2 w-100"><div><Switch checked={loginType === 'email'} onCheckedChange={handleLoginTypeChange} /></div>{/* 表单主体 */}<form id="login-form" onSubmit={form.handleSubmit(onSubmit)}><FieldGroup>{loginType === 'email' ? (<>{/* 邮箱字段 */}<Controllername="email"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-email">邮箱</FieldLabel><Input {...field} id="login-email" aria-invalid={fieldState.invalid} placeholder="请输入邮箱" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/>{/* 验证码字段 */}<Controllername="code"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-code">验证码</FieldLabel><Input {...field} id="login-code" aria-invalid={fieldState.invalid} placeholder="请输入6位验证码" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/></>) : (<>{/* 账号字段 */}<Controllername="account"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-account">账号</FieldLabel><Input {...field} id="login-account" aria-invalid={fieldState.invalid} placeholder="请输入账号" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/>{/* 密码字段 */}<Controllername="password"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-password">密码</FieldLabel><Input{...field}id="login-password"type="password"aria-invalid={fieldState.invalid}placeholder="请输入密码"/>{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/></>)}{/* 公共字段:登录感言 */}<Controllername="loginMessage"control={form.control}render={({ field, fieldState }) => (<Field data-invalid={fieldState.invalid}><FieldLabel htmlFor="login-message">登录感言(选填)</FieldLabel><Input {...field} id="login-message" aria-invalid={fieldState.invalid} placeholder="最多20个字" />{/* 显示验证错误信息 */}{fieldState.invalid && <FieldError errors={[fieldState.error]} />}</Field>)}/></FieldGroup></form>{/* 操作按钮组 */}<Field orientation="horizontal" className="mt-2"><Button type="button" variant="outline" onClick={() => form.reset()}>重置</Button><Button type="submit" form="login-form">提交</Button></Field></div>);
}

总结

Zod条件验证

不拆分组件但又想分离验证规则,有几个优雅的解决方案:

方案 1:使用 Zod 的条件验证(最优雅)

// 根据登录类型动态生成 schema
const getLoginSchema = (loginType: 'account' | 'email') => {if (loginType === 'account') {return z.object({account: z.string().min(5, '账号至少5个字符').max(32, '账号最多32个字符'),password: z.string().min(6, '密码至少6个字符').max(30, '密码最多30个字符'),});} else {return z.object({email: z.string().email('请输入有效的邮箱地址'),code: z.string().length(6, '验证码为6位数字'),});}
};// 在组件中使用
const [loginType, setLoginType] = useState<'account' | 'email'>('account');
const schema = getLoginSchema(loginType);const form = useForm({resolver: zodResolver(schema),defaultValues: loginType === 'account' ? { account: '', password: '' }: { email: '', code: '' },
});

方案 2:使用 Zod 的 discriminatedUnion(类型安全)

const loginSchema = z.discriminatedUnion('type', [z.object({type: z.literal('account'),account: z.string().min(5, '账号至少5个字符'),password: z.string().min(6, '密码至少6个字符'),}),z.object({type: z.literal('email'),email: z.string().email('请输入有效的邮箱'),code: z.string().length(6, '验证码为6位'),}),
]);
http://www.jsqmd.com/news/40538/

相关文章:

  • 完整教程:PDFBox - PDDocument 与 byte 数组、PDF 加密
  • Dark Side of the Moon
  • flask:自定义异常
  • 图片合集
  • OpenWrt路由的端口映射问题
  • 算法沉淀第七天(AtCoder Beginner Contest 428 和 小训练赛) - 详解
  • How-to-extract-text-from-PDF-Image-files-OCR-CarlZeng
  • Web应用模糊测试完全指南
  • 升鲜宝供应链管理系统、各端的访问地址及nginx 真实的配置方法
  • uiautomator2元素查看器WEditor的安装和启动
  • WEditor的使用方法
  • 【题解】LOJ6300. 「CodePlus 2018 3 月赛」博弈论与概率统计
  • 感情粉末沿着试管边缘 在祝福中逐渐分解 加热认知离子重新排列 于底部悲伤沉淀
  • C#循序渐进 - 详解
  • 2025.11.14 - A
  • 从RvmTranslator到PlantAssistant
  • MI50 在ubuntu 下 风扇控制实现
  • PortSwigger靶场之 CSRF where token is not tied to user session通关秘籍 - 实践
  • nvm不能下载安装低版本node解决办法
  • flask: 抛出异常
  • 20251114——读后感5
  • 雪地奔驰全等级提升所需经验一览
  • 2025皮肤亚健康管理品牌最新专业推荐:科技赋能健康美新生态
  • 【HT-086-Div.2】嗡嗡蜜蜂
  • 第四十一篇
  • 深入解析:Vue3 路由配置和使用与讲解(超级详细)
  • 好题集 (0) - 目录
  • 251114
  • HubSpot如何规模化推进AI编码助手应用
  • 好题集 (4) - CF487E Tourists