Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend setValue functionality #1513

Closed
14 changes: 7 additions & 7 deletions docs/api/formik.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ All three 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<Values>`

Form validation errors. Should match the shape of your form's `values` defined
in `initialValues`. If you are using `validationSchema` (which you should be),
Expand Down Expand Up @@ -115,7 +115,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<Values>) => void`

Set `errors` imperatively.

Expand Down Expand Up @@ -153,13 +153,13 @@ use it to pass API responses back into your component in `handleSubmit`.

Set `isSubmitting` imperatively.

#### `setTouched: (fields: { [field: string]: boolean }) => void`
#### `setTouched: (fields: FormikTouched<Values>) => void`

Set `touched` imperatively.

#### `setValues: (fields: { [field: string]: any }) => void`
#### `setValues: (valuesOrValuesFactory: ValuesOrValuesFactory<Values>) => void`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### `setValues: (valuesOrValuesFactory: ValuesOrValuesFactory<Values>) => void`
#### `setValues: (valuesOrValuesUpdater: Values | (v: Values) => void) => void`

I think this is clearer.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: We could just use React.SetStateAction<S>


Set `values` imperatively.
Set `values` imperatively. The argument you pass can be either a complete `Values` object, or a `ValuesFactory<Values>`: that is, a function which takes the existing values, and returns new values (`(prevValues: Readonly<Values>) => Values`). The latter is useful if you want to perform a partial update of values, rather than replacing `values`.

#### `status?: any`

Expand All @@ -170,11 +170,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<Values>`

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
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export const FieldLevelValidationExample = () => (

You can control when Formik runs validation by changing the values of `<Formik validateOnChange>` and/or `<Formik validateOnBlur>` 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`
Expand Down
11 changes: 8 additions & 3 deletions src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FormikProps,
FieldMetaProps,
FieldInputProps,
ValuesOrValuesFactory,
} from './types';
import {
isFunction,
Expand All @@ -32,7 +33,7 @@ type FormikMessage<Values> =
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SET_ISVALIDATING'; payload: boolean }
| { type: 'SET_ISSUBMITTING'; payload: boolean }
| { type: 'SET_VALUES'; payload: Values }
| { type: 'SET_VALUES'; payload: ValuesOrValuesFactory<Values> }
| { type: 'SET_FIELD_VALUE'; payload: { field: string; value?: any } }
| { type: 'SET_FIELD_TOUCHED'; payload: { field: string; value?: boolean } }
| { type: 'SET_FIELD_ERROR'; payload: { field: string; value?: string } }
Expand All @@ -49,7 +50,11 @@ function formikReducer<Values>(
) {
switch (msg.type) {
case 'SET_VALUES':
return { ...state, values: msg.payload };
const values = isFunction(msg.payload)
? msg.payload(state.values)
: msg.payload;

return { ...state, values };
case 'SET_TOUCHED':
return { ...state, touched: msg.payload };
case 'SET_ERRORS':
Expand Down Expand Up @@ -457,7 +462,7 @@ export function useFormik<Values extends FormikValues = FormikValues>({
}, []);

const setValues = useEventCallback(
(values: Values) => {
(values: ValuesOrValuesFactory<Values>) => {
dispatch({ type: 'SET_VALUES', payload: values });
return validateOnChange
? validateFormWithLowPriority(state.values)
Expand Down
5 changes: 4 additions & 1 deletion src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export interface FormikComputedProps<Values> {
readonly initialStatus?: any;
}

export type ValuesFactory<Values> = (prevValues: Readonly<Values>) => Values;
export type ValuesOrValuesFactory<Values> = Values | ValuesFactory<Values>;

samstarling marked this conversation as resolved.
Show resolved Hide resolved
/**
* Formik state helpers
*/
Expand All @@ -84,7 +87,7 @@ export interface FormikHelpers<Values> {
/** Manually set touched object */
setTouched(touched: FormikTouched<Values>): void;
/** Manually set values object */
setValues(values: Values): void;
setValues(ValuesOrValuesFactory: ValuesOrValuesFactory<Values>): void;
/** Set value of form field directly */
setFieldValue(
field: keyof Values & string,
Expand Down
28 changes: 22 additions & 6 deletions test/Formik.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jest.spyOn(global.console, 'warn');

interface Values {
name: string;
age?: number;
}

function Form({
Expand Down Expand Up @@ -43,10 +44,13 @@ function Form({
);
}

const InitialValues = { name: 'jared' };
const InitialValues = {
name: 'jared',
age: 30,
};

function renderFormik<V = Values>(props?: Partial<FormikConfig<V>>) {
let injected: any;
let injected: FormikProps<V>;
const { rerender, ...rest } = render(
<Formik
onSubmit={noop as any}
Expand Down Expand Up @@ -426,7 +430,7 @@ describe('<Formik>', () => {
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', () => {
Expand Down Expand Up @@ -581,8 +585,20 @@ describe('<Formik>', () => {
it('setValues sets values', () => {
const { getProps } = renderFormik<Values>();

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<Values>();

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 () => {
Expand Down Expand Up @@ -735,7 +751,7 @@ describe('<Formik>', () => {
const { getProps } = renderFormik();

expect(getProps().dirty).toBeFalsy();
getProps().setValues({ name: 'ian' });
getProps().setValues({ name: 'ian', age: 27 });
expect(getProps().dirty).toBeTruthy();
});

Expand Down Expand Up @@ -807,7 +823,7 @@ describe('<Formik>', () => {
getProps().handleReset();

expect(onReset).toHaveBeenCalledWith(
{ name: 'jared' },
InitialValues,
expect.objectContaining({
resetForm: expect.any(Function),
setErrors: expect.any(Function),
Expand Down