diff --git a/package.json b/package.json index 30df0953..482c1107 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@rc-component/async-validator": "^5.0.3", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.5.0", "clsx": "^2.1.1" }, "devDependencies": { diff --git a/src/useForm.ts b/src/useForm.ts index 5799eec1..e2cd4b70 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -1,4 +1,5 @@ import { merge } from '@rc-component/util/lib/utils/set'; +import { mergeWith } from '@rc-component/util'; import warning from '@rc-component/util/lib/warning'; import * as React from 'react'; import { HOOK_MARK } from './FieldContext'; @@ -783,10 +784,14 @@ export class FormStore { const { onValuesChange } = this.callbacks; if (onValuesChange) { + const fieldEntity = this.getFieldsMap(true).get(namePath); const changedValues = cloneByNamePathList(this.store, [namePath]); const allValues = this.getFieldsValue(); // Merge changedValues into allValues to ensure allValues contains the latest changes - const mergedAllValues = merge(allValues, changedValues); + const mergedAllValues = mergeWith([allValues, changedValues], { + // When value is array, it means trigger by Form.List which should replace directly + prepareArray: current => (fieldEntity?.isList() ? [] : [...(current || [])]), + }); onValuesChange(changedValues, mergedAllValues); } diff --git a/tests/list.test.tsx b/tests/list.test.tsx index e7670d84..8a207e24 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -1042,4 +1042,101 @@ describe('Form.List', () => { expect(onFinishFailed).toHaveBeenCalled(); }); + + it('List should have correct onValuesChange', () => { + const onValuesChange = jest.fn(); + + const [container] = generateForm( + fields => ( +
+ {fields.map(field => ( +
+ + + + + + +
+ ))} +
+ ), + { + initialValues: { + list: [{ first: 'light' }], + }, + onValuesChange, + }, + ); + + fireEvent.change(getInput(container, 1), { target: { value: 'little' } }); + expect(onValuesChange).toHaveBeenCalledWith( + { list: [{ last: 'little' }] }, + { list: [{ first: 'light', last: 'little' }] }, + ); + }); + + it('should correctly merge array-valued fields within Form.List items without losing data', async () => { + const TagInput = ({ value = [], onChange }: any) => ( + { + const newValue = e.target.value + .split(',') + .map(s => s.trim()) + .filter(Boolean); + onChange(newValue); + }} + /> + ); + + const onValuesChange = jest.fn(); + + const [container] = generateForm( + fields => ( + <> + {fields.map(field => ( +
+ + + + + + +
+ ))} + + ), + { + initialValues: { + list: [{ name: 'John', tags: ['react', 'js'] }], + }, + onValuesChange, + }, + ); + + const tagInput = container.querySelector('input[data-testid="tag-input"]') as HTMLElement; + + await act(async () => { + fireEvent.change(tagInput, { + target: { value: 'react,ts' }, + }); + }); + + expect(onValuesChange).toHaveBeenCalledWith( + { list: [{ tags: ['react', 'ts'] }] }, // changedValues + { list: [{ name: 'John', tags: ['react', 'ts'] }] }, // allValues + ); + onValuesChange.mockReset(); + + await act(async () => { + fireEvent.change(tagInput, { target: { value: 'react,ts,redux' } }); + }); + + expect(onValuesChange).toHaveBeenLastCalledWith( + { list: [{ tags: ['react', 'ts', 'redux'] }] }, + { list: [{ name: 'John', tags: ['react', 'ts', 'redux'] }] }, + ); + }); });