Formik FieldArray字段级验证:Yup运行时开关验证及替代方案
问题描述
场景
有3位参与者,所有参与者需填写相同的问题集,但每个问题都有「适用工单」列表,用于决定是否向特定参与者展示该问题。
核心需求
- 能否通过
yup在运行时关闭特定字段的验证?(目前了解到所有字段验证初始已定义,似乎不可行) - 还有哪些验证实现方案?可接受字段级验证,但希望保留
yup的内置校验能力
详细说明
使用Formik表单,需要条件渲染特定字段,要求字段不显示时移除其验证规则。表单包含多个通过Formik FieldArray处理的「子表单」,子表单内的字段可动态显示/隐藏,但验证规则不会随之变化。
目前尝试用yup.when()实现条件验证,虽能达到效果,但会移除表单中所有元素的该字段验证,代码如下:
/** * 给定适用工单列表,当"id"属性不在appliedTickets数组中时,关闭验证 */ if (field.appliedTickets.length > 0) { schema = schema.when("id", { is: (value: any) => field.appliedTickets.every((t) => t !== value), then: (s) => s.notRequired(), otherwise: (s) => s, }); }
更新1:补充Schema
主Schema
const personalDetailsSchema = useMemo(() => { const ticketSchema = { id: string().strip(), firstName: string() .trim() .required(getErrorMessage("firstName.label", { required: true })), lastName: string() .trim() .required(getErrorMessage("lastName.label", { required: true })), gender: mixed<Gender>() .required(getErrorMessage("gender.label", { required: true })) .defined() .default(Gender.male), email: string() .email("Must be a valid email") .required(getErrorMessage("email.label", { required: true })), dateOfBirth: date() .required(getErrorMessage("dateOfBirth.label", { required: true })) .typeError("A valid date is required") .nullable() .default(null), nationality: string() .oneOf<Country["abbr"]>( allCountries.map((c) => c.abbr), "An available nationality is required" ) .required(getErrorMessage("nationality.label", { required: true })) .defined(), extraInfo: customQuestionSchema ? object({ id: string().strip(), ...customQuestionSchema }) : mixed().notRequired(), }; const personalDetailsSchema = object({ tickets: array(object(ticketSchema)), }); return personalDetailsSchema; }, [getErrorMessage, customQuestionSchema]);
自定义问题Schema
每个自定义问题包含isPredefinedType属性,基于该属性,通过两个函数动态生成剩余Schema:
const customQuestionSchema: Nullable<Record<string, AnySchema>> = useMemo(() => { if (!customQuestions) { return; } const jointSchema = customQuestions?.reduce((previous, current) => { if (current.isPreDefinedType) { const presetQuestionSchema = getPredefinedSchemaFor(current); if (presetQuestionSchema) { return Object.assign(previous, { [presetQuestionSchema.name]: presetQuestionSchema.objectSchema, }); } } const { name, objectSchema } = getCustomSchemaFor(current); return Object.assign(previous, { [name]: objectSchema, }); }, {}); return jointSchema; }, [customQuestions, getCustomSchemaFor, getPredefinedSchemaFor]); const personalDetailsSchema = object({ tickets: array(object(ticketSchema)), })
解决方案
1. 关于通过Yup在运行时关闭特定字段验证
Yup本身支持基于字段值的动态验证,你之前的写法问题在于:schema.when("id")是对整个Schema生效,而非针对数组中的单个子表单元素。要实现只对特定参与者(数组中特定ticket项)的字段关闭验证,需要在数组项的Schema内部做条件判断:
方式一:字段级when关联当前ticket的id
在定义ticketSchema时,针对需要条件验证的字段,直接在字段内部使用when,关联当前ticket的id和该字段的适用工单列表:
// 示例:为自定义字段添加条件验证 customField: string() .when("id", { is: (ticketId) => field.appliedTickets.includes(ticketId), then: (schema) => schema.required("该字段必填"), otherwise: (schema) => schema.notRequired(), })
这样就能确保只有当当前ticket的id在适用列表中时,才会启用该字段的验证,不会影响其他ticket项。
方式二:自定义test验证逻辑
如果需要更灵活的判断,可以用Yup的test方法,直接获取当前表单上下文:
customField: string() .test( "required-if-applicable", "该字段必填", function(value) { const { id } = this.parent; // 获取当前ticket的id if (!field.appliedTickets.includes(id)) { return true; // 不适用时直接通过验证 } return !!value; // 适用时验证是否有值 } )
2. 其他验证实现方案
方案一:动态生成Schema
既然每个问题的适用工单是已知的,可以在生成customQuestionSchema时,直接根据适用工单列表动态决定是否添加验证规则:
const jointSchema = customQuestions?.reduce((previous, current) => { let fieldSchema; if (current.isPreDefinedType) { fieldSchema = getPredefinedSchemaFor(current).objectSchema; } else { fieldSchema = getCustomSchemaFor(current).objectSchema; } // 根据适用工单列表修改验证规则 if (current.appliedTickets.length > 0) { fieldSchema = fieldSchema.when("id", { is: (ticketId) => !current.appliedTickets.includes(ticketId), then: (s) => s.notRequired(), otherwise: (s) => s, }); } return { ...previous, [current.name]: fieldSchema }; }, {});
这样生成的Schema本身就包含了条件验证逻辑,无需运行时修改。
方案二:结合Formik字段级验证
Formik允许在validate函数中做字段级验证,同时调用Yup的验证方法复用内置规则:
const validate = (values) => { const errors = {}; // 遍历每个ticket values.tickets.forEach((ticket, index) => { errors.tickets = errors.tickets || []; // 遍历每个自定义问题 Object.keys(customQuestionSchema || {}).forEach((fieldName) => { const field = customQuestions.find(q => q.name === fieldName); // 如果当前ticket不在适用列表中,跳过验证 if (!field.appliedTickets.includes(ticket.id)) return; // 用Yup验证该字段 try { customQuestionSchema[fieldName].validateSync(ticket.extraInfo[fieldName]); } catch (err) { errors.tickets[index] = errors.tickets[index] || {}; errors.tickets[index].extraInfo = errors.tickets[index].extraInfo || {}; errors.tickets[index].extraInfo[fieldName] = err.message; } }); }); return errors; }; // 在Formik组件中使用 <Formik initialValues={...} validate={validate} onSubmit={...} > {/* 表单内容 */} </Formik>
这种方式可以完全控制哪些字段需要验证,同时保留Yup的内置校验能力。
方案三:手动控制Formik验证状态
在字段显示/隐藏时,调用Formik的setFieldError清除或添加错误:
// 字段隐藏时清除错误 const handleFieldHide = (ticketIndex, fieldName) => { props.setFieldError(`tickets[${ticketIndex}].extraInfo.${fieldName}`, undefined); };
适合需要手动干预验证状态的场景,配合条件渲染使用。
内容的提问来源于stack exchange,提问作者AiSirachcha21

