Skip to content

Commit

Permalink
fix: errors in FieldArray being returned as arrays (#3780)
Browse files Browse the repository at this point in the history
* fix: errors in FieldArray being returned as arrays

Fixes: #2993

* chore: cleanup, add changeset

---------

Co-authored-by: Chris Getsfred <cgetsfred@ediscovery.co>
  • Loading branch information
quantizor and Chris Getsfred authored May 26, 2023
1 parent 3de6d7d commit 9c75a9f
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-peas-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'formik': patch
---

Fixed an issue with array field errors being incorrectly split into an array of individual characters instead of an array of error strings.
34 changes: 26 additions & 8 deletions packages/formik/src/FieldArray.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import * as React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import * as React from 'react';
import isEqual from 'react-fast-compare';
import { connect } from './connect';
import {
FormikContextType,
FormikProps,
FormikState,
SharedRenderProps,
FormikProps,
} from './types';
import {
getIn,
isEmptyArray,
isEmptyChildren,
isFunction,
isObject,
setIn,
isEmptyArray,
} from './utils';
import isEqual from 'react-fast-compare';

export type FieldArrayRenderProps = ArrayHelpers & {
form: FormikProps<any>;
Expand Down Expand Up @@ -118,6 +119,24 @@ const copyArrayLike = (arrayLike: ArrayLike<any>) => {
}
};

const createAlterationHandler = (
alteration: boolean | Function,
defaultFunction: Function
) => {
const fn = typeof alteration === 'function' ? alteration : defaultFunction;

return (data: any | any[]) => {
if (Array.isArray(data) || isObject(data)) {
const clone = copyArrayLike(data);
return fn(clone);
}

// This can be assumed to be a primitive, which
// is a case for top level validation errors
return data;
};
};

class FieldArrayInner<Values = {}> extends React.Component<
FieldArrayConfig & { formik: FormikContextType<Values> },
{}
Expand Down Expand Up @@ -160,9 +179,8 @@ class FieldArrayInner<Values = {}> extends React.Component<
formik: { setFormikState },
} = this.props;
setFormikState((prevState: FormikState<any>) => {
let updateErrors = typeof alterErrors === 'function' ? alterErrors : fn;
let updateTouched =
typeof alterTouched === 'function' ? alterTouched : fn;
let updateErrors = createAlterationHandler(alterErrors, fn);
let updateTouched = createAlterationHandler(alterTouched, fn);

// values fn should be executed before updateErrors and updateTouched,
// otherwise it causes an error with unshift.
Expand Down Expand Up @@ -305,7 +323,7 @@ class FieldArrayInner<Values = {}> extends React.Component<
this.updateArrayField(
// so this gets call 3 times
(array: any[]) => {
const tmp = array;
const tmp = array.slice();
if (!result) {
result = tmp && tmp.pop && tmp.pop();
}
Expand Down
276 changes: 275 additions & 1 deletion packages/formik/test/FieldArray.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
import * as Yup from 'yup';

import { FieldArray, Formik, isFunction } from '../src';

Expand Down Expand Up @@ -424,4 +425,277 @@ describe('<FieldArray />', () => {
expect(formikBag.values.friends).toEqual(finalExpected);
});
});

describe('schema validation', () => {
const schema = Yup.object({
friends: Yup.array(Yup.string().required()).required().min(3),
});

let formikBag: any;
let arrayHelpers: any;

beforeEach(() => {
render(
<Formik
initialValues={{ friends: [] }}
onSubmit={noop}
validationSchema={schema}
validateOnMount
>
{(props: any) => {
formikBag = props;
return (
<FieldArray name="friends">
{arrayProps => {
arrayHelpers = arrayProps;
return null;
}}
</FieldArray>
);
}}
</Formik>
);
});

describe('props.push()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('');
arrayHelpers.push('andrea');
});

expect(formikBag.errors.friends).toHaveLength(3);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBeUndefined();
expect(formikBag.errors.friends[2]).toBe(
'friends[2] is a required field'
);
});
});

describe('props.swap()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.swap(0, 1);
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('');
arrayHelpers.push('andrea');
arrayHelpers.swap(1, 2);
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
});
});

describe('props.move()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.move(0, 1);
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('');
arrayHelpers.push('andrea');
arrayHelpers.move(1, 2);
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
});
});

describe('props.insert()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.insert(1, 'brian');
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('andrea');
arrayHelpers.insert(1, '');
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
});
});

describe('props.unshift()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.unshift('brian');
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('');
arrayHelpers.push('brian');
arrayHelpers.push('andrea');

arrayHelpers.unshift('michael');
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
});
});

describe('props.remove()', () => {
it('should return error string with top level violation ', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('andrea');
arrayHelpers.remove(0);
arrayHelpers.remove(0);
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push(''); // index specific violation
arrayHelpers.push('andrea');
arrayHelpers.remove(0);
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
expect(formikBag.errors.friends[2]).toBeUndefined();
expect(formikBag.errors.friends[4]).toBeUndefined();
});
});

describe('props.pop()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('andrea');
arrayHelpers.pop();
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.push('brian');
arrayHelpers.push('');
arrayHelpers.push('andrea');
arrayHelpers.pop();
});

expect(formikBag.errors.friends).toHaveLength(3);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBeUndefined();
expect(formikBag.errors.friends[2]).toBe(
'friends[2] is a required field'
);
});
});

describe('props.replace()', () => {
it('should return error string with top level violation', async () => {
await act(async () => {
await arrayHelpers.push('michael');
arrayHelpers.replace(0, 'brian');
});

expect(formikBag.errors.friends).toBe(
'friends field must have at least 3 items'
);
});

it('should return errors array with nested value violation', async () => {
await act(async () => {
arrayHelpers.unshift('michael');
await arrayHelpers.push('brian');
arrayHelpers.push('andrea');
arrayHelpers.push('jared');

await arrayHelpers.replace(1, '');
});

expect(formikBag.errors.friends).toHaveLength(2);
expect(formikBag.errors.friends[0]).toBeUndefined();
expect(formikBag.errors.friends[1]).toBe(
'friends[1] is a required field'
);
});
});
});
});

1 comment on commit 9c75a9f

@vercel
Copy link

@vercel vercel bot commented on 9c75a9f May 26, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

formik-docs – ./website

formik-docs-git-master-jared.vercel.app
formik-docs.vercel.app
formik-docs-jared.vercel.app
www.formik.org
formik.org

Please sign in to comment.