diff --git a/.changeset/spotty-poets-bathe.md b/.changeset/spotty-poets-bathe.md new file mode 100644 index 000000000..8228beca7 --- /dev/null +++ b/.changeset/spotty-poets-bathe.md @@ -0,0 +1,5 @@ +--- +'formik': patch +--- + +Value of setFieldValue can be a function that takes previous field value diff --git a/docs/api/formik.md b/docs/api/formik.md index 09c0c159b..9fa07f3a4 100644 --- a/docs/api/formik.md +++ b/docs/api/formik.md @@ -193,7 +193,7 @@ Trigger a form submission. The promise will be rejected if form is invalid. Number of times user tried to submit the form. Increases when [`handleSubmit`](#handlesubmit-e-reactformeventhtmlformelement--void) is called, resets after calling [`handleReset`](#handlereset---void). `submitCount` is readonly computed property and should not be mutated directly. -#### `setFieldValue: (field: string, value: any, shouldValidate?: boolean) => Promise` +#### `setFieldValue: (field: string, value: React.SetStateAction, shouldValidate?: boolean) => Promise` Set the value of a field imperatively. `field` should match the key of `values` you wish to update. Useful for creating custom input change handlers. Calling this will trigger validation to run if `validateOnChange` is set to `true` (which it is by default). You can also explicitly prevent/skip validation by passing a third argument as `false`. diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index 80d868665..0dd631bb7 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -582,18 +582,20 @@ export function useFormik({ ); const setFieldValue = useEventCallback( - (field: string, value: any, shouldValidate?: boolean) => { + (field: string, value: React.SetStateAction, shouldValidate?: boolean) => { + const resolvedValue = isFunction(value) ? value(state.values[field]) : value; + dispatch({ type: 'SET_FIELD_VALUE', payload: { field, - value, + value: resolvedValue, }, }); const willValidate = shouldValidate === undefined ? validateOnChange : shouldValidate; return willValidate - ? validateFormWithHighPriority(setIn(state.values, field, value)) + ? validateFormWithHighPriority(setIn(state.values, field, resolvedValue)) : Promise.resolve(); } ); diff --git a/packages/formik/test/Formik.test.tsx b/packages/formik/test/Formik.test.tsx index 98b6ef3c9..864589643 100644 --- a/packages/formik/test/Formik.test.tsx +++ b/packages/formik/test/Formik.test.tsx @@ -740,6 +740,55 @@ describe('', () => { }); }); + it('setFieldValue sets value by key when takes a setter function', async () => { + const { getProps, rerender } = renderFormik(); + + act(() => { + getProps().setFieldValue('name', (prev: string) => { + return prev + ' chronicus'; + }); + }); + rerender(); + await waitFor(() => { + expect(getProps().values.name).toEqual('jared chronicus'); + }); + }); + + it( + 'setFieldValue should run validations with resolved value when takes a setter function and validateOnChange is true (default)', + async () => { + const validate = jest.fn(() =>({})); + const { getProps, rerender } = renderFormik({ validate }); + + act(() => { + getProps().setFieldValue('name', (prev: string) => prev + ' chronicus'); + }); + rerender(); + await waitFor(() => { + // the validate function is called with the second arg as undefined always in this case + expect(validate).toHaveBeenCalledWith(expect.objectContaining({ + name: 'jared chronicus', + }), undefined); + }); + } + ); + + it('setFieldValue should NOT run validations when takes a setter function and validateOnChange is false', async () => { + const validate = jest.fn(); + const { getProps, rerender } = renderFormik({ + validate, + validateOnChange: false, + }); + + act(() => { + getProps().setFieldValue('name', (prev: string) => prev + ' chronicus'); + }); + rerender(); + await waitFor(() => { + expect(validate).not.toHaveBeenCalled(); + }); + }); + it('setTouched sets touched', () => { const { getProps } = renderFormik();