Skip to content

Commit 267736f

Browse files
authored
fix: correctly mutate deep field array item and trigger validation (#3974)
1 parent 70ddc5b commit 267736f

3 files changed

Lines changed: 88 additions & 8 deletions

File tree

packages/vee-validate/src/useFieldArray.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Ref, unref, ref, computed, onBeforeUnmount } from 'vue';
1+
import { Ref, unref, ref, onBeforeUnmount } from 'vue';
22
import { isNullOrUndefined } from '../../shared';
33
import { FormContextKey } from './symbols';
44
import { FieldArrayContext, FieldEntry, MaybeRef, PrivateFieldArrayContext } from './types';
5-
import { getFromPath, injectWithSelf, warn } from './utils';
5+
import { computedDeep, getFromPath, injectWithSelf, warn } from './utils';
66

77
export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): FieldArrayContext<TValue> {
88
const form = injectWithSelf(FormContextKey, undefined);
@@ -64,7 +64,7 @@ export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): Fi
6464

6565
const entry: FieldEntry<TValue> = {
6666
key,
67-
value: computed<TValue>({
67+
value: computedDeep<TValue>({
6868
get() {
6969
const currentValues = getFromPath<TValue[]>(form?.values, unref(arrayPath), []) || [];
7070
const idx = fields.value.findIndex(e => e.key === key);
@@ -172,7 +172,9 @@ export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): Fi
172172
if (!Array.isArray(pathValue) || pathValue.length - 1 < idx) {
173173
return;
174174
}
175+
175176
form?.setFieldValue(`${pathName}[${idx}]`, value);
177+
form?.validate({ mode: 'validated-only' });
176178
}
177179

178180
function prepend(value: TValue) {

packages/vee-validate/src/utils/common.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { getCurrentInstance, inject, InjectionKey, warn as vueWarning } from 'vue';
2-
import isEqual from 'fast-deep-equal';
1+
import { getCurrentInstance, inject, InjectionKey, ref, Ref, warn as vueWarning, watch } from 'vue';
2+
import { klona as deepCopy } from 'klona/full';
33
import { isIndex, isNullOrUndefined, isObject, toNumber } from '../../../shared';
4-
import { isContainerValue, isEmptyContainer, isNotNestedPath } from './assertions';
4+
import { isContainerValue, isEmptyContainer, isEqual, isNotNestedPath } from './assertions';
55
import { PrivateFieldContext } from '../types';
66

77
function cleanupNonNestedPath(path: string) {
@@ -265,3 +265,37 @@ export function withLatest<TFunction extends (...args: any[]) => Promise<any>, T
265265
return result;
266266
};
267267
}
268+
269+
export function computedDeep<TValue = unknown>({ get, set }: { get(): TValue; set(value: TValue): void }): Ref<TValue> {
270+
const baseRef = ref(deepCopy(get())) as Ref<TValue>;
271+
272+
watch(
273+
get,
274+
newValue => {
275+
if (isEqual(newValue, baseRef.value)) {
276+
return;
277+
}
278+
279+
baseRef.value = deepCopy(newValue);
280+
},
281+
{
282+
deep: true,
283+
}
284+
);
285+
286+
watch(
287+
baseRef,
288+
newValue => {
289+
if (isEqual(newValue, get())) {
290+
return;
291+
}
292+
293+
set(deepCopy(newValue));
294+
},
295+
{
296+
deep: true,
297+
}
298+
);
299+
300+
return baseRef;
301+
}

packages/vee-validate/tests/useFieldArray.spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useForm, useFieldArray } from '@/vee-validate';
2-
import { onMounted } from 'vue';
1+
import { useForm, useFieldArray, FieldEntry } from '@/vee-validate';
2+
import { onMounted, Ref } from 'vue';
3+
import * as yup from 'yup';
34
import { mountWithHoc, flushPromises } from './helpers';
45

56
test('can update a field entry model directly', async () => {
@@ -30,6 +31,49 @@ test('can update a field entry model directly', async () => {
3031
expect(document.querySelector('p')?.innerHTML).toBe('test');
3132
});
3233

34+
test('can update a field entry deep model directly and validate it', async () => {
35+
let fields!: Ref<FieldEntry<{ name: string }>[]>;
36+
mountWithHoc({
37+
setup() {
38+
const { errors } = useForm({
39+
validateOnMount: true,
40+
validationSchema: yup.object({
41+
users: yup.array().of(
42+
yup.object({
43+
name: yup.string().required(),
44+
})
45+
),
46+
}),
47+
initialValues: {
48+
users: [{ name: '' }],
49+
},
50+
});
51+
52+
fields = useFieldArray<{ name: string }>('users').fields;
53+
54+
return {
55+
fields,
56+
errors,
57+
};
58+
},
59+
template: `
60+
<p>{{ fields[0].value.name }}</p>
61+
<span>{{ errors }}</span>
62+
`,
63+
});
64+
65+
await flushPromises();
66+
expect(document.querySelector('p')?.innerHTML).toBe('');
67+
expect(document.querySelector('span')?.innerHTML).toBeTruthy();
68+
69+
const item = fields.value[0];
70+
item.value.name = 'test';
71+
72+
await flushPromises();
73+
expect(document.querySelector('p')?.innerHTML).toBe('test');
74+
expect(document.querySelector('span')?.innerHTML).toBe('{}');
75+
});
76+
3377
test('warns when updating a no-longer existing item', async () => {
3478
const spy = jest.spyOn(console, 'warn').mockImplementation();
3579
mountWithHoc({

0 commit comments

Comments
 (0)