Skip to content

Commit c4415f8

Browse files
committed
fix: ensure field's meta.required is reactive closes #4738
1 parent 10fccfd commit c4415f8

File tree

9 files changed

+184
-39
lines changed

9 files changed

+184
-39
lines changed

.changeset/silly-frogs-grab.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'vee-validate': patch
3+
'@vee-validate/valibot': patch
4+
'@vee-validate/yup': patch
5+
'@vee-validate/zod': patch
6+
---
7+
8+
fix: ensure meta.required is reactive whenever the schema changes closes #4738

packages/valibot/src/index.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,36 @@ export function toTypedSchema<
5656
return values;
5757
},
5858
describe(path) {
59-
if (!path) {
59+
try {
60+
if (!path) {
61+
return {
62+
required: !queryOptional(valibotSchema),
63+
exists: true,
64+
};
65+
}
66+
67+
const pathSchema = getSchemaForPath(path, valibotSchema);
68+
if (!pathSchema) {
69+
return {
70+
required: false,
71+
exists: false,
72+
};
73+
}
74+
6075
return {
61-
required: !queryOptional(valibotSchema),
76+
required: !queryOptional(pathSchema),
6277
exists: true,
6378
};
64-
}
79+
} catch {
80+
if (__DEV__) {
81+
console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
82+
}
6583

66-
const pathSchema = getSchemaForPath(path, valibotSchema);
67-
if (!pathSchema) {
6884
return {
6985
required: false,
7086
exists: false,
7187
};
7288
}
73-
74-
return {
75-
required: !queryOptional(pathSchema),
76-
exists: true,
77-
};
7889
},
7990
};
8091

packages/valibot/tests/valibot.spec.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Ref } from 'vue';
2-
import { useField, useForm } from '@/vee-validate';
1+
import { Ref, ref } from 'vue';
2+
import { FieldMeta, useField, useForm } from '@/vee-validate';
33
import { string, minLength, email as emailV, object, coerce, any, number, optional, array } from 'valibot';
44
import { toTypedSchema } from '@/valibot';
55
import { mountWithHoc, flushPromises, setValue } from '../../vee-validate/tests/helpers';
@@ -71,6 +71,40 @@ describe('valibot', () => {
7171
expect(errors.value).toEqual([REQUIRED_MSG, MIN_MSG]);
7272
});
7373

74+
test('reports required state reactively', async () => {
75+
let meta!: FieldMeta<any>;
76+
const schema = ref(
77+
toTypedSchema(
78+
object({
79+
name: string(),
80+
}),
81+
),
82+
);
83+
84+
mountWithHoc({
85+
setup() {
86+
useForm({
87+
validationSchema: schema,
88+
});
89+
90+
const field = useField('name');
91+
meta = field.meta;
92+
93+
return {
94+
schema,
95+
};
96+
},
97+
template: `<div></div>`,
98+
});
99+
100+
await flushPromises();
101+
await expect(meta.required).toBe(true);
102+
103+
schema.value = toTypedSchema(object({ name: optional(string()) }));
104+
await flushPromises();
105+
await expect(meta.required).toBe(false);
106+
});
107+
74108
test('shows multiple errors using error bag', async () => {
75109
const wrapper = mountWithHoc({
76110
setup() {

packages/vee-validate/src/useFieldState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { computed, isRef, reactive, ref, Ref, unref, watch, MaybeRef, MaybeRefOrGetter } from 'vue';
1+
import { computed, isRef, reactive, ref, Ref, unref, watch, MaybeRef, MaybeRefOrGetter, toValue } from 'vue';
22
import { FieldMeta, FieldState, FieldValidator, InputType, PrivateFormContext, PathState } from './types';
33
import { getFromPath, isEqual, normalizeErrorItem } from './utils';
44
import { TypedSchema } from '../dist/vee-validate';
@@ -212,7 +212,7 @@ function createFieldMeta<TValue>(
212212
errors: Ref<string[]>,
213213
schema?: TypedSchema<TValue>,
214214
) {
215-
const isRequired = schema?.describe?.().required ?? false;
215+
const isRequired = computed(() => toValue(schema)?.describe?.().required ?? false);
216216

217217
const meta = reactive({
218218
touched: false,

packages/vee-validate/src/useForm.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,15 @@ export function useForm<
296296
}
297297

298298
const isRequired = computed(() => {
299-
if (isTypedSchema(schema)) {
300-
return (schema as TypedSchema).describe?.(toValue(path)).required ?? false;
299+
const schemaValue = toValue(schema);
300+
if (isTypedSchema(schemaValue)) {
301+
return schemaValue.describe?.(toValue(path)).required ?? false;
301302
}
302303

303304
// Path own schema
304-
if (isTypedSchema(config?.schema)) {
305-
return (config?.schema as TypedSchema).describe?.().required ?? false;
305+
const configSchemaValue = toValue(config?.schema);
306+
if (isTypedSchema(configSchemaValue)) {
307+
return configSchemaValue.describe?.().required ?? false;
306308
}
307309

308310
return false;

packages/yup/src/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,30 @@ export function toTypedSchema<TSchema extends Schema, TOutput = InferType<TSchem
6767
}
6868
},
6969
describe(path) {
70-
if (!path) {
71-
return getDescriptionFromYupSpec(yupSchema.spec);
72-
}
70+
try {
71+
if (!path) {
72+
return getDescriptionFromYupSpec(yupSchema.spec);
73+
}
74+
75+
const description = getSpecForPath(path, yupSchema);
76+
if (!description) {
77+
return {
78+
required: false,
79+
exists: false,
80+
};
81+
}
82+
83+
return getDescriptionFromYupSpec(description);
84+
} catch {
85+
if (__DEV__) {
86+
console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
87+
}
7388

74-
const description = getSpecForPath(path, yupSchema);
75-
if (!description) {
7689
return {
7790
required: false,
7891
exists: false,
7992
};
8093
}
81-
82-
return getDescriptionFromYupSpec(description);
8394
},
8495
};
8596

packages/yup/tests/yup.spec.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Ref } from 'vue';
2-
import { useField, useForm } from '@/vee-validate';
1+
import { Ref, ref } from 'vue';
2+
import { FieldMeta, useField, useForm } from '@/vee-validate';
33
import { toTypedSchema } from '@/yup';
44
import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers';
55
import * as yup from 'yup';
@@ -389,6 +389,40 @@ test('reports required state on fields', async () => {
389389
);
390390
});
391391

392+
test('reports required state reactively', async () => {
393+
let meta!: FieldMeta<any>;
394+
const schema = ref(
395+
toTypedSchema(
396+
yup.object({
397+
name: yup.string().required(),
398+
}),
399+
),
400+
);
401+
402+
mountWithHoc({
403+
setup() {
404+
useForm({
405+
validationSchema: schema,
406+
});
407+
408+
const field = useField('name');
409+
meta = field.meta;
410+
411+
return {
412+
schema,
413+
};
414+
},
415+
template: `<div></div>`,
416+
});
417+
418+
await flushPromises();
419+
await expect(meta.required).toBe(true);
420+
421+
schema.value = toTypedSchema(yup.object({ name: yup.string() }));
422+
await flushPromises();
423+
await expect(meta.required).toBe(false);
424+
});
425+
392426
test('reports required false for non-existent fields', async () => {
393427
const metaSpy = vi.fn();
394428
mountWithHoc({

packages/zod/src/index.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,25 +55,36 @@ export function toTypedSchema<
5555
}
5656
},
5757
describe(path) {
58-
if (!path) {
58+
try {
59+
if (!path) {
60+
return {
61+
required: !zodSchema.isOptional(),
62+
exists: true,
63+
};
64+
}
65+
66+
const description = getSchemaForPath(path, zodSchema);
67+
if (!description) {
68+
return {
69+
required: false,
70+
exists: false,
71+
};
72+
}
73+
5974
return {
60-
required: !zodSchema.isOptional(),
75+
required: !description.isOptional(),
6176
exists: true,
6277
};
63-
}
78+
} catch {
79+
if (__DEV__) {
80+
console.warn(`Failed to describe path ${path} on the schema, returning a default description.`);
81+
}
6482

65-
const description = getSchemaForPath(path, zodSchema);
66-
if (!description) {
6783
return {
6884
required: false,
6985
exists: false,
7086
};
7187
}
72-
73-
return {
74-
required: !description.isOptional(),
75-
exists: true,
76-
};
7788
},
7889
};
7990

packages/zod/tests/zod.spec.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useField, useForm } from '@/vee-validate';
1+
import { FieldMeta, useField, useForm } from '@/vee-validate';
22
import { toTypedSchema } from '@/zod';
33
import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers';
4-
import { Ref } from 'vue';
4+
import { Ref, ref } from 'vue';
55
import { z } from 'zod';
66

77
const REQUIRED_MSG = 'field is required';
@@ -68,6 +68,40 @@ test('generates multiple errors for any given field', async () => {
6868
expect(errors.value).toEqual([REQUIRED_MSG, MIN_MSG]);
6969
});
7070

71+
test('reports required state reactively', async () => {
72+
let meta!: FieldMeta<any>;
73+
const schema = ref(
74+
toTypedSchema(
75+
z.object({
76+
name: z.string(),
77+
}),
78+
),
79+
);
80+
81+
mountWithHoc({
82+
setup() {
83+
useForm({
84+
validationSchema: schema,
85+
});
86+
87+
const field = useField('name');
88+
meta = field.meta;
89+
90+
return {
91+
schema,
92+
};
93+
},
94+
template: `<div></div>`,
95+
});
96+
97+
await flushPromises();
98+
await expect(meta.required).toBe(true);
99+
100+
schema.value = toTypedSchema(z.object({ name: z.string().optional() }));
101+
await flushPromises();
102+
await expect(meta.required).toBe(false);
103+
});
104+
71105
test('shows multiple errors using error bag', async () => {
72106
const wrapper = mountWithHoc({
73107
setup() {

0 commit comments

Comments
 (0)