Skip to content

Commit e354a13

Browse files
committedJul 8, 2023
fix: normalize error paths to use brackets for indices closes #4211
1 parent 4e11ff9 commit e354a13

File tree

6 files changed

+86
-29
lines changed

6 files changed

+86
-29
lines changed
 

Diff for: ‎.changeset/seven-news-begin.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'vee-validate': patch
3+
'@vee-validate/zod': patch
4+
---
5+
6+
fix: Normalize error paths to use brackets for indices closes #4211

Diff for: ‎packages/shared/utils.ts

+22
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,25 @@ export function merge(target: any, source: any) {
6565

6666
return target;
6767
}
68+
69+
/**
70+
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
71+
*/
72+
export function normalizeFormPath(path: string): string {
73+
const pathArr = path.split('.');
74+
if (!pathArr.length) {
75+
return '';
76+
}
77+
78+
let fullPath = String(pathArr[0]);
79+
for (let i = 1; i < pathArr.length; i++) {
80+
if (isIndex(pathArr[i])) {
81+
fullPath += `[${pathArr[i]}]`;
82+
continue;
83+
}
84+
85+
fullPath += `.${pathArr[i]}`;
86+
}
87+
88+
return fullPath;
89+
}

Diff for: ‎packages/vee-validate/src/useForm.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ import {
6464
normalizeErrorItem,
6565
normalizeEventValue,
6666
omit,
67+
isPathsEqual,
6768
} from './utils';
6869
import { FormContextKey } from './symbols';
6970
import { validateTypedSchema, validateObjectSchema } from './validate';
7071
import { refreshInspector, registerFormWithDevTools } from './devtools';
71-
import { isCallable, merge } from '../../shared';
72+
import { isCallable, merge, normalizeFormPath } from '../../shared';
7273
import { getConfig } from './config';
7374
import { PartialDeep } from 'type-fest';
7475

@@ -143,7 +144,7 @@ export function useForm<
143144
const state = findPathState(field);
144145
if (!state) {
145146
if (typeof field === 'string') {
146-
extraErrorsBag.value[field] = normalizeErrorItem(message);
147+
extraErrorsBag.value[normalizeFormPath(field) as Path<TValues>] = normalizeErrorItem(message);
147148
}
148149
return;
149150
}
@@ -242,7 +243,7 @@ export function useForm<
242243
config?: Partial<PathStateConfig>
243244
): PathState<TValue> {
244245
const initialValue = computed(() => getFromPath(initialValues.value, toValue(path)));
245-
const pathStateExists = pathStates.value.find(state => state.path === unref(path));
246+
const pathStateExists = pathStates.value.find(s => isPathsEqual(s.path, toValue(path)));
246247
if (pathStateExists) {
247248
if (config?.type === 'checkbox' || config?.type === 'radio') {
248249
pathStateExists.multiple = true;
@@ -383,7 +384,7 @@ export function useForm<
383384
}
384385

385386
function findPathState<TPath extends Path<TValues>>(path: TPath | PathState) {
386-
const pathState = typeof path === 'string' ? pathStates.value.find(state => state.path === path) : path;
387+
const pathState = typeof path === 'string' ? pathStates.value.find(s => isPathsEqual(s.path, path)) : path;
387388

388389
return pathState as PathState<PathValue<TValues, TPath>> | undefined;
389390
}
@@ -493,7 +494,7 @@ export function useForm<
493494
handleSubmit.withControlled = makeSubmissionFactory(true);
494495

495496
function removePathState<TPath extends Path<TValues>>(path: TPath, id: number) {
496-
const idx = pathStates.value.findIndex(s => s.path === path);
497+
const idx = pathStates.value.findIndex(s => isPathsEqual(s.path, path));
497498
const pathState = pathStates.value[idx];
498499
if (idx === -1 || !pathState) {
499500
return;

Diff for: ‎packages/vee-validate/src/utils/assertions.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Locator, TypedSchema, YupSchema } from '../types';
2-
import { isCallable, isObject } from '../../../shared';
2+
import { isCallable, isObject, normalizeFormPath } from '../../../shared';
33
import { IS_ABSENT } from '../symbols';
44

55
export const isClient = typeof window !== 'undefined';
@@ -186,3 +186,7 @@ export function isFile(a: unknown): a is File {
186186

187187
return a instanceof File;
188188
}
189+
190+
export function isPathsEqual(lhs: string, rhs: string) {
191+
return normalizeFormPath(lhs) === normalizeFormPath(rhs);
192+
}

Diff for: ‎packages/vee-validate/tests/useForm.spec.ts

+45
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mountWithHoc, setValue, flushPromises, runInSetup, dispatchEvent } from
33
import * as yup from 'yup';
44
import { onMounted, Ref } from 'vue';
55
import { ModelComp, CustomModelComp } from './helpers/ModelComp';
6+
import { FieldContext } from '../dist/vee-validate';
67

78
describe('useForm()', () => {
89
const REQUIRED_MESSAGE = 'Field is required';
@@ -1203,4 +1204,48 @@ describe('useForm()', () => {
12031204
await flushPromises();
12041205
expect(form.meta.value.dirty).toBe(false);
12051206
});
1207+
1208+
describe('error paths can have dot or square bracket for the same field', () => {
1209+
test('path is bracket, mutations are dot', async () => {
1210+
let field!: FieldContext<unknown>;
1211+
let errorSetter!: FormContext['setFieldError'];
1212+
mountWithHoc({
1213+
setup() {
1214+
const { setFieldError } = useForm();
1215+
field = useField('users[0].test');
1216+
errorSetter = setFieldError;
1217+
return {};
1218+
},
1219+
template: `<div></div>`,
1220+
});
1221+
1222+
await flushPromises();
1223+
expect(field.errorMessage.value).toBe(undefined);
1224+
await flushPromises();
1225+
errorSetter('users.0.test', 'error');
1226+
await flushPromises();
1227+
expect(field.errorMessage.value).toBe('error');
1228+
});
1229+
1230+
test('path is dot, mutations are bracket', async () => {
1231+
let field!: FieldContext<unknown>;
1232+
let errorSetter!: FormContext['setFieldError'];
1233+
mountWithHoc({
1234+
setup() {
1235+
const { setFieldError } = useForm();
1236+
field = useField('users.0.test');
1237+
errorSetter = setFieldError;
1238+
return {};
1239+
},
1240+
template: `<div></div>`,
1241+
});
1242+
1243+
await flushPromises();
1244+
expect(field.errorMessage.value).toBe(undefined);
1245+
await flushPromises();
1246+
errorSetter('users[0].test', 'error');
1247+
await flushPromises();
1248+
expect(field.errorMessage.value).toBe('error');
1249+
});
1250+
});
12061251
});

Diff for: ‎packages/zod/src/index.ts

+2-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ZodObject, input, output, ZodDefault, ZodSchema, ParseParams, ZodIssue } from 'zod';
22
import { PartialDeep } from 'type-fest';
33
import type { TypedSchema, TypedSchemaError } from 'vee-validate';
4-
import { isIndex, isObject, merge } from '../../shared';
4+
import { isObject, merge, normalizeFormPath } from '../../shared';
55

66
/**
77
* Transforms a Zod object schema to Yup's schema
@@ -49,7 +49,7 @@ export function toTypedSchema<
4949

5050
function processIssues(issues: ZodIssue[], errors: Record<string, TypedSchemaError>): void {
5151
issues.forEach(issue => {
52-
const path = joinPath(issue.path);
52+
const path = normalizeFormPath(issue.path.join('.'));
5353
if (issue.code === 'invalid_union') {
5454
processIssues(
5555
issue.unionErrors.flatMap(ue => ue.issues),
@@ -69,27 +69,6 @@ function processIssues(issues: ZodIssue[], errors: Record<string, TypedSchemaErr
6969
});
7070
}
7171

72-
/**
73-
* Constructs a path with brackets to be compatible with vee-validate path syntax
74-
*/
75-
function joinPath(path: (string | number)[]): string {
76-
if (!path.length) {
77-
return '';
78-
}
79-
80-
let fullPath = String(path[0]);
81-
for (let i = 1; i < path.length; i++) {
82-
if (isIndex(path[i])) {
83-
fullPath += `[${path[i]}]`;
84-
continue;
85-
}
86-
87-
fullPath += `.${path[i]}`;
88-
}
89-
90-
return fullPath;
91-
}
92-
9372
// Zod does not support extracting default values so the next best thing is manually extracting them.
9473
// https://github.com/colinhacks/zod/issues/1944#issuecomment-1406566175
9574
function getDefaults<Schema extends ZodSchema>(schema: Schema): unknown {

0 commit comments

Comments
 (0)
Please sign in to comment.