Repository: logaretm/vee-validate
Pull request #4741 (2024-05-05 13:17:04)

docs: replaces deprecated zod.nonempty() with zod.min(1)
Method has been deprecated for a while. min(1) is the suggested replacement. https://github.com/colinhacks/zod/discussions/2847

🔎 __Overview__
Updates docs to align with zod. Package does not add additional methods to zod.

✔ __Issues affected__
No issues


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/logaretm/vee-validate/pull/4741?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.87%. Comparing base [(`10fccfd`)](https://app.codecov.io/gh/logaretm/vee-validate/commit/10fccfd354ff4c93d7b478e6bba690360d6fd477?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad) to head [(`7cf2a7e`)](https://app.codecov.io/gh/logaretm/vee-validate/pull/4741?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #4741   +/-   ##
=======================================
  Coverage   89.87%   89.87%           
=======================================
  Files          93       93           
  Lines        7676     7676           
  Branches     1360     1360           
=======================================
  Hits         6899     6899           
  Misses        770      770           
  Partials        7        7           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/logaretm/vee-validate/pull/4741?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abdelrahman+Awad).


Patch:
diff --git a/.changeset/silly-frogs-grab.md b/.changeset/silly-frogs-grab.md
new file mode 100644
index 00000000..98a0cb56
--- /dev/null
+++ b/.changeset/silly-frogs-grab.md
@@ -0,0 +1,8 @@
+---
+'vee-validate': patch
+'@vee-validate/valibot': patch
+'@vee-validate/yup': patch
+'@vee-validate/zod': patch
+---
+
+fix: ensure meta.required is reactive whenever the schema changes closes #4738
diff --git a/docs/src/pages/integrations/zod-schema-validation.mdx b/docs/src/pages/integrations/zod-schema-validation.mdx
index 9d690394..5d95c089 100644
--- a/docs/src/pages/integrations/zod-schema-validation.mdx
+++ b/docs/src/pages/integrations/zod-schema-validation.mdx
@@ -74,7 +74,7 @@ import { toTypedSchema } from '@vee-validate/zod';
 import * as zod from 'zod';
 
 const fieldSchema = toTypedSchema(
-  zod.string().nonempty('Field is required').email({ message: 'Must be a valid email' })
+  zod.string().min(1, { message: 'Field is required' }).email({ message: 'Must be a valid email' })
 );
 const { value, errorMessage } = useField('email', fieldSchema);
 </script>
@@ -108,8 +108,8 @@ import * as zod from 'zod';
 
 const validationSchema = toTypedSchema(
   zod.object({
-    email: zod.string().nonempty('This is required').email({ message: 'Must be a valid email' }),
-    password: zod.string().nonempty('This is required').min(8, { message: 'Too short' }),
+    email: zod.string().min(1, { message: 'This is required' }).email({ message: 'Must be a valid email' }),
+    password: zod.string().min(1, { message: 'This is required' }).min(8, { message: 'Too short' }),
   })
 );
 
@@ -141,8 +141,8 @@ import * as zod from 'zod';
 
 const validationSchema = toTypedSchema(
   zod.object({
-    email: zod.string().nonempty('This is required').email({ message: 'Must be a valid email' }),
-    password: zod.string().nonempty('This is required').min(8, { message: 'Too short' }),
+    email: zod.string().min(1, { message: 'This is required' }).email({ message: 'Must be a valid email' }),
+    password: zod.string().min(1, { message: 'This is required' }).min(8, { message: 'Too short' }),
   })
 );
 
diff --git a/packages/valibot/src/index.ts b/packages/valibot/src/index.ts
index 086b4b20..ee0e317b 100644
--- a/packages/valibot/src/index.ts
+++ b/packages/valibot/src/index.ts
@@ -56,25 +56,36 @@ export function toTypedSchema<
       return values;
     },
     describe(path) {
-      if (!path) {
+      try {
+        if (!path) {
+          return {
+            required: !queryOptional(valibotSchema),
+            exists: true,
+          };
+        }
+
+        const pathSchema = getSchemaForPath(path, valibotSchema);
+        if (!pathSchema) {
+          return {
+            required: false,
+            exists: false,
+          };
+        }
+
         return {
-          required: !queryOptional(valibotSchema),
+          required: !queryOptional(pathSchema),
           exists: true,
         };
-      }
+      } catch {
+        if (__DEV__) {
+          console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
+        }
 
-      const pathSchema = getSchemaForPath(path, valibotSchema);
-      if (!pathSchema) {
         return {
           required: false,
           exists: false,
         };
       }
-
-      return {
-        required: !queryOptional(pathSchema),
-        exists: true,
-      };
     },
   };
 
diff --git a/packages/vee-validate/src/useFieldState.ts b/packages/vee-validate/src/useFieldState.ts
index f10d0ca4..42713d47 100644
--- a/packages/vee-validate/src/useFieldState.ts
+++ b/packages/vee-validate/src/useFieldState.ts
@@ -1,4 +1,4 @@
-import { computed, isRef, reactive, ref, Ref, unref, watch, MaybeRef, MaybeRefOrGetter } from 'vue';
+import { computed, isRef, reactive, ref, Ref, unref, watch, MaybeRef, MaybeRefOrGetter, toValue } from 'vue';
 import { FieldMeta, FieldState, FieldValidator, InputType, PrivateFormContext, PathState } from './types';
 import { getFromPath, isEqual, normalizeErrorItem } from './utils';
 import { TypedSchema } from '../dist/vee-validate';
@@ -212,7 +212,7 @@ function createFieldMeta<TValue>(
   errors: Ref<string[]>,
   schema?: TypedSchema<TValue>,
 ) {
-  const isRequired = schema?.describe?.().required ?? false;
+  const isRequired = computed(() => toValue(schema)?.describe?.().required ?? false);
 
   const meta = reactive({
     touched: false,
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index 5592e6e1..6dfd3221 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -296,13 +296,15 @@ export function useForm<
     }
 
     const isRequired = computed(() => {
-      if (isTypedSchema(schema)) {
-        return (schema as TypedSchema).describe?.(toValue(path)).required ?? false;
+      const schemaValue = toValue(schema);
+      if (isTypedSchema(schemaValue)) {
+        return schemaValue.describe?.(toValue(path)).required ?? false;
       }
 
       // Path own schema
-      if (isTypedSchema(config?.schema)) {
-        return (config?.schema as TypedSchema).describe?.().required ?? false;
+      const configSchemaValue = toValue(config?.schema);
+      if (isTypedSchema(configSchemaValue)) {
+        return configSchemaValue.describe?.().required ?? false;
       }
 
       return false;
diff --git a/packages/yup/src/index.ts b/packages/yup/src/index.ts
index 3ece1876..736aa69a 100644
--- a/packages/yup/src/index.ts
+++ b/packages/yup/src/index.ts
@@ -67,19 +67,30 @@ export function toTypedSchema<TSchema extends Schema, TOutput = InferType<TSchem
       }
     },
     describe(path) {
-      if (!path) {
-        return getDescriptionFromYupSpec(yupSchema.spec);
-      }
+      try {
+        if (!path) {
+          return getDescriptionFromYupSpec(yupSchema.spec);
+        }
+
+        const description = getSpecForPath(path, yupSchema);
+        if (!description) {
+          return {
+            required: false,
+            exists: false,
+          };
+        }
+
+        return getDescriptionFromYupSpec(description);
+      } catch {
+        if (__DEV__) {
+          console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
+        }
 
-      const description = getSpecForPath(path, yupSchema);
-      if (!description) {
         return {
           required: false,
           exists: false,
         };
       }
-
-      return getDescriptionFromYupSpec(description);
     },
   };
 
diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts
index 99794bc9..48399b12 100644
--- a/packages/zod/src/index.ts
+++ b/packages/zod/src/index.ts
@@ -55,25 +55,36 @@ export function toTypedSchema<
       }
     },
     describe(path) {
-      if (!path) {
+      try {
+        if (!path) {
+          return {
+            required: !zodSchema.isOptional(),
+            exists: true,
+          };
+        }
+
+        const description = getSchemaForPath(path, zodSchema);
+        if (!description) {
+          return {
+            required: false,
+            exists: false,
+          };
+        }
+
         return {
-          required: !zodSchema.isOptional(),
+          required: !description.isOptional(),
           exists: true,
         };
-      }
+      } catch {
+        if (__DEV__) {
+          console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
+        }
 
-      const description = getSchemaForPath(path, zodSchema);
-      if (!description) {
         return {
           required: false,
           exists: false,
         };
       }
-
-      return {
-        required: !description.isOptional(),
-        exists: true,
-      };
     },
   };
 


Test patch:
diff --git a/packages/valibot/tests/valibot.spec.ts b/packages/valibot/tests/valibot.spec.ts
index 2bf03c0b..f40eafdc 100644
--- a/packages/valibot/tests/valibot.spec.ts
+++ b/packages/valibot/tests/valibot.spec.ts
@@ -1,5 +1,5 @@
-import { Ref } from 'vue';
-import { useField, useForm } from '@/vee-validate';
+import { Ref, ref } from 'vue';
+import { FieldMeta, useField, useForm } from '@/vee-validate';
 import { string, minLength, email as emailV, object, coerce, any, number, optional, array } from 'valibot';
 import { toTypedSchema } from '@/valibot';
 import { mountWithHoc, flushPromises, setValue } from '../../vee-validate/tests/helpers';
@@ -71,6 +71,40 @@ describe('valibot', () => {
     expect(errors.value).toEqual([REQUIRED_MSG, MIN_MSG]);
   });
 
+  test('reports required state reactively', async () => {
+    let meta!: FieldMeta<any>;
+    const schema = ref(
+      toTypedSchema(
+        object({
+          name: string(),
+        }),
+      ),
+    );
+
+    mountWithHoc({
+      setup() {
+        useForm({
+          validationSchema: schema,
+        });
+
+        const field = useField('name');
+        meta = field.meta;
+
+        return {
+          schema,
+        };
+      },
+      template: `<div></div>`,
+    });
+
+    await flushPromises();
+    await expect(meta.required).toBe(true);
+
+    schema.value = toTypedSchema(object({ name: optional(string()) }));
+    await flushPromises();
+    await expect(meta.required).toBe(false);
+  });
+
   test('shows multiple errors using error bag', async () => {
     const wrapper = mountWithHoc({
       setup() {
diff --git a/packages/yup/tests/yup.spec.ts b/packages/yup/tests/yup.spec.ts
index bad35b41..7f8db7c6 100644
--- a/packages/yup/tests/yup.spec.ts
+++ b/packages/yup/tests/yup.spec.ts
@@ -1,5 +1,5 @@
-import { Ref } from 'vue';
-import { useField, useForm } from '@/vee-validate';
+import { Ref, ref } from 'vue';
+import { FieldMeta, useField, useForm } from '@/vee-validate';
 import { toTypedSchema } from '@/yup';
 import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers';
 import * as yup from 'yup';
@@ -389,6 +389,40 @@ test('reports required state on fields', async () => {
   );
 });
 
+test('reports required state reactively', async () => {
+  let meta!: FieldMeta<any>;
+  const schema = ref(
+    toTypedSchema(
+      yup.object({
+        name: yup.string().required(),
+      }),
+    ),
+  );
+
+  mountWithHoc({
+    setup() {
+      useForm({
+        validationSchema: schema,
+      });
+
+      const field = useField('name');
+      meta = field.meta;
+
+      return {
+        schema,
+      };
+    },
+    template: `<div></div>`,
+  });
+
+  await flushPromises();
+  await expect(meta.required).toBe(true);
+
+  schema.value = toTypedSchema(yup.object({ name: yup.string() }));
+  await flushPromises();
+  await expect(meta.required).toBe(false);
+});
+
 test('reports required false for non-existent fields', async () => {
   const metaSpy = vi.fn();
   mountWithHoc({
diff --git a/packages/zod/tests/zod.spec.ts b/packages/zod/tests/zod.spec.ts
index ed6a863a..a25342dd 100644
--- a/packages/zod/tests/zod.spec.ts
+++ b/packages/zod/tests/zod.spec.ts
@@ -1,7 +1,7 @@
-import { useField, useForm } from '@/vee-validate';
+import { FieldMeta, useField, useForm } from '@/vee-validate';
 import { toTypedSchema } from '@/zod';
 import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers';
-import { Ref } from 'vue';
+import { Ref, ref } from 'vue';
 import { z } from 'zod';
 
 const REQUIRED_MSG = 'field is required';
@@ -68,6 +68,40 @@ test('generates multiple errors for any given field', async () => {
   expect(errors.value).toEqual([REQUIRED_MSG, MIN_MSG]);
 });
 
+test('reports required state reactively', async () => {
+  let meta!: FieldMeta<any>;
+  const schema = ref(
+    toTypedSchema(
+      z.object({
+        name: z.string(),
+      }),
+    ),
+  );
+
+  mountWithHoc({
+    setup() {
+      useForm({
+        validationSchema: schema,
+      });
+
+      const field = useField('name');
+      meta = field.meta;
+
+      return {
+        schema,
+      };
+    },
+    template: `<div></div>`,
+  });
+
+  await flushPromises();
+  await expect(meta.required).toBe(true);
+
+  schema.value = toTypedSchema(z.object({ name: z.string().optional() }));
+  await flushPromises();
+  await expect(meta.required).toBe(false);
+});
+
 test('shows multiple errors using error bag', async () => {
   const wrapper = mountWithHoc({
     setup() {