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>
{/* 邮箱字段 */}
<Controller
name="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>
)}
/>
{/* 验证码字段 */}
<Controller
name="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>
{/* 账号字段 */}
<Controller
name="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>
)}
/>
{/* 密码字段 */}
<Controller
name="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' ? (
<>
{/* 邮箱字段 */}
<Controller
name="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>
)}
/>
{/* 验证码字段 */}
<Controller
name="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>
)}
/>
</>
) : (
<>
{/* 账号字段 */}
<Controller
name="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>
)}
/>
{/* 密码字段 */}
<Controller
name="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>
)}
/>
</>
)}
{/* 公共字段:登录感言 */}
<Controller
name="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位'),
}),
]);

浙公网安备 33010602011771号