From ad1d008a149ed444adb1c3c56c6e2b4a7734817a Mon Sep 17 00:00:00 2001 From: Patricio Beltran Date: Thu, 1 Oct 2020 13:53:05 -0700 Subject: [PATCH] Extend setValue to use React.SetStateAction functionality (#2556) This is a continuation of https://github.com/formik/formik/pull/1513 which was closed for inactivity. This PR has addressed all the comments and reused React.SetStateAction types. With this, formik consumers could pass a value or a function to setValues just like you can with React's setState dispatchers. --- docs/src/pages/docs/2.1.4/api/formik.md | 10 +++---- .../src/pages/docs/2.1.4/guides/validation.md | 2 +- packages/formik/src/Formik.tsx | 10 +++++-- packages/formik/src/types.tsx | 2 +- packages/formik/test/Formik.test.tsx | 28 +++++++++++++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/src/pages/docs/2.1.4/api/formik.md b/docs/src/pages/docs/2.1.4/api/formik.md index 91441953e..4537e30a9 100644 --- a/docs/src/pages/docs/2.1.4/api/formik.md +++ b/docs/src/pages/docs/2.1.4/api/formik.md @@ -66,7 +66,7 @@ Each render methods will be passed the same props: Returns `true` if values are not deeply equal from initial values, `false` otherwise. `dirty` is a readonly computed property and should not be mutated directly. -#### `errors: { [field: string]: string }` +#### `errors: FormikErrors` Form validation errors. Should match the shape of your form's `values` defined in `initialValues`. If you are using `validationSchema` (which you should be), @@ -114,7 +114,7 @@ Returns `true` if Formik is running validation during submission, or by calling Imperatively reset the form. If `nextInitialState` is specified, Formik will set this state as the new "initial state" and use the related values of `nextInitialState` to update the form's `initialValues` as well as `initialTouched`, `initialStatus`, `initialErrors`. This is useful for altering the initial state (i.e. "base") of the form after changes have been made. If `nextInitialState` is not defined, then Formik will reset state to the original initial state. The latter is useful for calling `resetForm` within `componentDidUpdate` or `useEffect`. -#### `setErrors: (fields: { [field: string]: string }) => void` +#### `setErrors: (fields: FormikErrors) => void` Set `errors` imperatively. @@ -156,7 +156,7 @@ Set `isSubmitting` imperatively. You would call it with `setSubmitting(false)` i Set `touched` imperatively. Calling this will trigger validation to run if `validateOnBlur` is set to `true` (which it is by default). You can also explicitly prevent/skip validation by passing a second argument as `false`. -#### `setValues: (fields: { [field: string]: any }, shouldValidate?: boolean) => void` +#### `setValues: (fields: React.SetStateAction, shouldValidate?: boolean) => void` Set `values` imperatively. 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 second argument as `false`. @@ -169,11 +169,11 @@ and passing through API responses to your inner component. `status` should only be modified by calling [`setStatus`](#setstatus-status-any-void). -#### `touched: { [field: string]: boolean }` +#### `touched: FormikTouched` Touched fields. Each key corresponds to a field that has been touched/visited. -#### `values: { [field: string]: any }` +#### `values: Values` Your form's values. Will have the shape of the result of `mapPropsToValues` (if specified) or all props that are not functions passed to your wrapped diff --git a/docs/src/pages/docs/2.1.4/guides/validation.md b/docs/src/pages/docs/2.1.4/guides/validation.md index 124adb2d4..245111426 100644 --- a/docs/src/pages/docs/2.1.4/guides/validation.md +++ b/docs/src/pages/docs/2.1.4/guides/validation.md @@ -256,7 +256,7 @@ export const FieldLevelValidationExample = () => ( You can control when Formik runs validation by changing the values of `` and/or `` props depending on your needs. By default, Formik will run validation methods as follows: -**After "change" events/methods** (things that update`values`) +**After "change" events/methods** (things that update `values`) - `handleChange` - `setFieldValue` diff --git a/packages/formik/src/Formik.tsx b/packages/formik/src/Formik.tsx index f20eb5133..d405dba71 100755 --- a/packages/formik/src/Formik.tsx +++ b/packages/formik/src/Formik.tsx @@ -581,12 +581,16 @@ export function useFormik({ }, []); const setValues = useEventCallback( - (values: Values, shouldValidate?: boolean) => { - dispatch({ type: 'SET_VALUES', payload: values }); + (values: React.SetStateAction, shouldValidate?: boolean) => { + const resolvedValues = isFunction(values) + ? values(state.values) + : values; + + dispatch({ type: 'SET_VALUES', payload: resolvedValues }); const willValidate = shouldValidate === undefined ? validateOnChange : shouldValidate; return willValidate - ? validateFormWithLowPriority(values) + ? validateFormWithLowPriority(resolvedValues) : Promise.resolve(); } ); diff --git a/packages/formik/src/types.tsx b/packages/formik/src/types.tsx index 54eaa0e2b..9acd04885 100644 --- a/packages/formik/src/types.tsx +++ b/packages/formik/src/types.tsx @@ -87,7 +87,7 @@ export interface FormikHelpers { shouldValidate?: boolean ) => void; /** Manually set values object */ - setValues: (values: Values, shouldValidate?: boolean) => void; + setValues: (values: React.SetStateAction, shouldValidate?: boolean) => void; /** Set value of form field directly */ setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void; /** Set error message of a form field directly */ diff --git a/packages/formik/test/Formik.test.tsx b/packages/formik/test/Formik.test.tsx index 248ecd4dc..d1d7f13af 100644 --- a/packages/formik/test/Formik.test.tsx +++ b/packages/formik/test/Formik.test.tsx @@ -14,6 +14,7 @@ jest.spyOn(global.console, 'warn'); interface Values { name: string; + age?: number; } function Form({ @@ -48,10 +49,13 @@ function Form({ ); } -const InitialValues = { name: 'jared' }; +const InitialValues = { + name: 'jared', + age: 30, +}; function renderFormik(props?: Partial>) { - let injected: any; + let injected: FormikProps; const { rerender, ...rest } = render( ', () => { expect(getProps().touched).toEqual({}); fireEvent.submit(getByTestId('form')); - expect(getProps().touched).toEqual({ name: true }); + expect(getProps().touched).toEqual({ name: true, age: true }); }); it('should push submission state changes to child component', () => { @@ -609,8 +613,20 @@ describe('', () => { it('setValues sets values', () => { const { getProps } = renderFormik(); - getProps().setValues({ name: 'ian' }); + getProps().setValues({ name: 'ian', age: 25 }); expect(getProps().values.name).toEqual('ian'); + expect(getProps().values.age).toEqual(25); + }); + + it('setValues takes a function which can patch values', () => { + const { getProps } = renderFormik(); + + getProps().setValues((values: Values) => ({ + ...values, + age: 80, + })); + expect(getProps().values.name).toEqual('jared'); + expect(getProps().values.age).toEqual(80); }); it('setValues should run validations when validateOnChange is true (default)', async () => { @@ -765,7 +781,7 @@ describe('', () => { const { getProps } = renderFormik(); expect(getProps().dirty).toBeFalsy(); - getProps().setValues({ name: 'ian' }); + getProps().setValues({ name: 'ian', age: 27 }); expect(getProps().dirty).toBeTruthy(); }); @@ -853,7 +869,7 @@ describe('', () => { getProps().resetForm(); expect(onReset).toHaveBeenCalledWith( - { name: 'jared' }, + InitialValues, expect.objectContaining({ resetForm: expect.any(Function), setErrors: expect.any(Function),