From aed0b2478d78d66008555dd5cb2ec97dc7d358e5 Mon Sep 17 00:00:00 2001 From: Evan Jacobs Date: Fri, 26 May 2023 13:04:41 -0400 Subject: [PATCH] feat: provide current values as context to yup validation by default Yup by default only allows for cross-field validation within the same field object. This is not that useful in most scenarios because a sufficiently-complex form will have several `yup.object()` in the schema. ```ts const deepNestedSchema = Yup.object({ object: Yup.object({ nestedField: Yup.number().required(), }), object2: Yup.object({ // this doesn't work because `object.nestedField` is outside of `object2` nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('object.nestedField')), }), }); ``` However, Yup offers something called `context` which can operate across the entire schema when using a $ prefix: ```ts const deepNestedSchema = Yup.object({ object: Yup.object({ nestedField: Yup.number().required(), }), object2: Yup.object({ // this works because of the "context" feature, enabled by $ prefix nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')), }), }); ``` With this change, you may now validate against any field in the entire schema, regardless of position when using the $ prefix. --- .changeset/lemon-crabs-explain.md | 38 ++++++++++++++++++++++ packages/formik/src/Formik.tsx | 9 +++--- packages/formik/test/yupHelpers.test.ts | 42 +++++++++++++++++++------ 3 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 .changeset/lemon-crabs-explain.md diff --git a/.changeset/lemon-crabs-explain.md b/.changeset/lemon-crabs-explain.md new file mode 100644 index 000000000..c12f5fee6 --- /dev/null +++ b/.changeset/lemon-crabs-explain.md @@ -0,0 +1,38 @@ +--- +'formik': minor +--- + +Yup by default only allows for cross-field validation within the +same field object. This is not that useful in most scenarios because +a sufficiently-complex form will have several `yup.object()` in the +schema. + +```ts +const deepNestedSchema = Yup.object({ + object: Yup.object({ + nestedField: Yup.number().required(), + }), + object2: Yup.object({ + // this doesn't work because `object.nestedField` is outside of `object2` + nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('object.nestedField')), + }), +}); +``` + +However, Yup offers something called `context` which can operate across +the entire schema when using a \$ prefix: + +```ts +const deepNestedSchema = Yup.object({ + object: Yup.object({ + nestedField: Yup.number().required(), + }), + object2: Yup.object({ + // this works because of the "context" feature, enabled by $ prefix + nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')), + }), +}); +``` + +With this change, you may now validate against any field in the entire schema, +regardless of position when using the \$ prefix. diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index d952a9035..98800a227 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -1066,12 +1066,13 @@ export function validateYupSchema( values: T, schema: any, sync: boolean = false, - context: any = {} + context?: any ): Promise> { - const validateData: FormikValues = prepareDataForValidation(values); - return schema[sync ? 'validateSync' : 'validate'](validateData, { + const normalizedValues: FormikValues = prepareDataForValidation(values); + + return schema[sync ? 'validateSync' : 'validate'](normalizedValues, { abortEarly: false, - context: context, + context: context || normalizedValues, }); } diff --git a/packages/formik/test/yupHelpers.test.ts b/packages/formik/test/yupHelpers.test.ts index 0c6ff2f9e..15261e186 100644 --- a/packages/formik/test/yupHelpers.test.ts +++ b/packages/formik/test/yupHelpers.test.ts @@ -1,16 +1,24 @@ +import * as Yup from 'yup'; import { validateYupSchema, yupToFormErrors } from '../src'; -const Yup = require('yup'); const schema = Yup.object().shape({ - name: Yup.string('Name must be a string').required('required'), - field: Yup.string('Field must be a string'), + name: Yup.string().required('required'), + field: Yup.string(), }); + const nestedSchema = Yup.object().shape({ object: Yup.object().shape({ - nestedField: Yup.string('Field must be a string'), - nestedArray: Yup.array().of( - Yup.string('Field must be a string').nullable() - ), + nestedField: Yup.string(), + nestedArray: Yup.array().of(Yup.string().nullable(true)), + }), +}); + +const deepNestedSchema = Yup.object({ + object: Yup.object({ + nestedField: Yup.number().required(), + }), + object2: Yup.object({ + nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')), }), }); @@ -32,8 +40,10 @@ describe('Yup helpers', () => { try { await validateYupSchema({}, schema); } catch (e) { - expect(e.name).toEqual('ValidationError'); - expect(e.errors).toEqual(['required']); + const err = e as Yup.ValidationError; + + expect(err.name).toEqual('ValidationError'); + expect(err.errors).toEqual(['required']); } }); @@ -85,5 +95,19 @@ describe('Yup helpers', () => { throw e; } }); + + it('should provide current values as context to enable deep object field validation', async () => { + try { + await validateYupSchema( + { object: { nestedField: 23 }, object2: { nestedFieldWithRef: 24 } }, + deepNestedSchema + ); + } catch (e) { + expect((e as Yup.ValidationError).name).toEqual('ValidationError'); + expect((e as Yup.ValidationError).errors).toEqual([ + 'object2.nestedFieldWithRef must be less than or equal to 23', + ]); + } + }); }); });