Skip to content

Commit c3c40e5

Browse files
authored
feat(4.6): Allow mutating field array iterable's value property (#3618) (#3759)
1 parent a52f133 commit c3c40e5

File tree

4 files changed

+132
-10
lines changed

4 files changed

+132
-10
lines changed

packages/vee-validate/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComputedRef, DeepReadonly, Ref } from 'vue';
1+
import { ComputedRef, Ref } from 'vue';
22
import { SchemaOf, AnySchema, AnyObjectSchema } from 'yup';
33
import { FieldValidationMetaInfo } from '../../shared';
44

@@ -58,7 +58,7 @@ export interface FieldEntry<TValue = unknown> {
5858
}
5959

6060
export interface FieldArrayContext<TValue = unknown> {
61-
fields: DeepReadonly<Ref<FieldEntry<TValue>[]>>;
61+
fields: Ref<FieldEntry<TValue>[]>;
6262
remove(idx: number): void;
6363
replace(newArray: TValue[]): void;
6464
update(idx: number, value: TValue): void;

packages/vee-validate/src/useFieldArray.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ref, unref, ref, readonly, computed, onBeforeUnmount } from 'vue';
1+
import { Ref, unref, ref, computed, onBeforeUnmount } from 'vue';
22
import { isNullOrUndefined } from '../../shared';
33
import { FormContextKey } from './symbols';
44
import { FieldArrayContext, FieldEntry, MaybeRef } from './types';
@@ -13,7 +13,7 @@ export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): Fi
1313
// eslint-disable-next-line @typescript-eslint/no-empty-function
1414
const noOp = () => {};
1515
const noOpApi: FieldArrayContext<TValue> = {
16-
fields: readonly(fields),
16+
fields,
1717
remove: noOp,
1818
push: noOp,
1919
swap: noOp,
@@ -61,11 +61,22 @@ export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): Fi
6161

6262
const entry: FieldEntry<TValue> = {
6363
key,
64-
value: computed<TValue>(() => {
65-
const currentValues = getFromPath<TValue[]>(form?.values, unref(arrayPath), []);
66-
const idx = fields.value.findIndex(e => e.key === key);
67-
68-
return idx === -1 ? value : currentValues[idx];
64+
value: computed<TValue>({
65+
get() {
66+
const currentValues = getFromPath<TValue[]>(form?.values, unref(arrayPath), []);
67+
const idx = fields.value.findIndex(e => e.key === key);
68+
69+
return idx === -1 ? value : currentValues[idx];
70+
},
71+
set(value: TValue) {
72+
const idx = fields.value.findIndex(e => e.key === key);
73+
if (idx === -1) {
74+
warn(`Attempting to update a non-existent array item`);
75+
return;
76+
}
77+
78+
update(idx, value);
79+
},
6980
}) as any, // will be auto unwrapped
7081
isFirst: false,
7182
isLast: false,
@@ -209,7 +220,7 @@ export function useFieldArray<TValue = unknown>(arrayPath: MaybeRef<string>): Fi
209220
});
210221

211222
return {
212-
fields: readonly(fields),
223+
fields,
213224
remove,
214225
push,
215226
swap,

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,58 @@ test('can update an item value at a given array index', async () => {
470470
expect(getValue(inputAt(1))).toBe('updated');
471471
});
472472

473+
test('can update an item value directly with .value setter', async () => {
474+
const onSubmit = jest.fn();
475+
mountWithHoc({
476+
setup() {
477+
const initial = {
478+
users: [
479+
{
480+
name: 'first',
481+
},
482+
],
483+
};
484+
485+
return {
486+
initial,
487+
onSubmit,
488+
};
489+
},
490+
template: `
491+
<VForm :initial-values="initial" @submit="onSubmit">
492+
<FieldArray name="users" v-slot="{ fields }">
493+
<div v-for="(field, idx) in fields" :key="field.key">
494+
<input v-model="fields[idx].value.name" />
495+
</div>
496+
</FieldArray>
497+
498+
<button>Submit</button>
499+
</VForm>
500+
`,
501+
});
502+
503+
await flushPromises();
504+
const inputAt = (idx: number) => (document.querySelectorAll('input') || [])[idx] as HTMLInputElement;
505+
506+
expect(getValue(inputAt(0))).toBe('first');
507+
setValue(inputAt(0), 'updated');
508+
await flushPromises();
509+
expect(getValue(inputAt(0))).toBe('updated');
510+
document.querySelector('button')?.click();
511+
await flushPromises();
512+
513+
expect(onSubmit).toHaveBeenLastCalledWith(
514+
expect.objectContaining({
515+
users: [
516+
{
517+
name: 'updated',
518+
},
519+
],
520+
}),
521+
expect.anything()
522+
);
523+
});
524+
473525
test('adds items to the start of the array with prepend()', async () => {
474526
const onSubmit = jest.fn();
475527
mountWithHoc({
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useForm, useFieldArray } from '@/vee-validate';
2+
import { onMounted } from 'vue';
3+
import { mountWithHoc, flushPromises } from './helpers';
4+
5+
test('can update a field entry model directly', async () => {
6+
mountWithHoc({
7+
setup() {
8+
useForm({
9+
initialValues: {
10+
users: ['1'],
11+
},
12+
});
13+
14+
const { fields } = useFieldArray('users');
15+
onMounted(() => {
16+
const item = fields.value[0];
17+
item.value = 'test';
18+
});
19+
20+
return {
21+
fields,
22+
};
23+
},
24+
template: `
25+
<p>{{ fields[0].value }}</p>
26+
`,
27+
});
28+
29+
await flushPromises();
30+
expect(document.querySelector('p')?.innerHTML).toBe('test');
31+
});
32+
33+
test('warns when updating a no-longer existing item', async () => {
34+
const spy = jest.spyOn(console, 'warn').mockImplementation();
35+
mountWithHoc({
36+
setup() {
37+
useForm({
38+
initialValues: {
39+
users: ['1'],
40+
},
41+
});
42+
43+
const { remove, fields } = useFieldArray('users');
44+
onMounted(() => {
45+
const item = fields.value[0];
46+
remove(0);
47+
item.value = 'test';
48+
});
49+
},
50+
template: `
51+
<div></div>
52+
`,
53+
});
54+
55+
await flushPromises();
56+
57+
expect(spy).toHaveBeenCalled();
58+
spy.mockRestore();
59+
});

0 commit comments

Comments
 (0)