Skip to content

Commit

Permalink
jaredpalmer#417 Tweak validation within FieldArray, rename dlv to get…
Browse files Browse the repository at this point in the history
…In (jaredpalmer#419)

* jaredpalmer#417 Tweak validation within FieldArray, rename dlv to getIn

* Update TOC
  • Loading branch information
jaredpalmer authored Feb 9, 2018
1 parent 2937582 commit 0d6b765
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 183 deletions.
214 changes: 142 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,81 +306,81 @@ npm install yup --save
** Table of Contents **

<!-- START doctoc generated TOC please keep comment here to allow auto update -->

<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

* [Guides](#guides)
* [Basics](#basics)
* [React Native](#react-native)
* [Why use `setFieldValue` instead of `handleChange`?](#why-use-setfieldvalue-instead-of-handlechange)
* [Avoiding new functions in render](#avoiding-new-functions-in-render)
* [Using Formik with TypeScript](#using-formik-with-typescript)
* [Render props (`<Formik />` and `<Field/>`)](#render-props-formik--and-field)
* [`withFormik()`](#withformik)
* [API](#api)
* [`<Formik />`](#formik-)
* [Formik render methods](#formik-render-methods)
* [Formik props](#formik-props)
* [`dirty: boolean`](#dirty-boolean)
* [`errors: { [field: string]: string }`](#errors--field-string-string-)
* [`handleBlur: (e: any) => void`](#handleblur-e-any--void)
* [`handleChange: (e: React.ChangeEvent<any>) => void`](#handlechange-e-reactchangeeventany--void)
* [`handleReset: () => void`](#handlereset---void)
* [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
* [`isSubmitting: boolean`](#issubmitting-boolean)
* [`isValid: boolean`](#isvalid-boolean)
* [`resetForm: (nextValues?: Values) => void`](#resetform-nextvalues-values--void)
* [`setErrors: (fields: { [field: string]: string }) => void`](#seterrors-fields--field-string-string---void)
* [`setFieldError: (field: string, errorMsg: string) => void`](#setfielderror-field-string-errormsg-string--void)
* [`setFieldTouched: (field: string, isTouched: boolean, shouldValidate?: boolean) => void`](#setfieldtouched-field-string-istouched-boolean-shouldvalidate-boolean--void)
* [`setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void`](#setfieldvalue-field-string-value-any-shouldvalidate-boolean--void)
* [`setStatus: (status?: any) => void`](#setstatus-status-any--void)
* [`setSubmitting: (isSubmitting: boolean) => void`](#setsubmitting-issubmitting-boolean--void)
* [`setTouched: (fields: { [field: string]: boolean }) => void`](#settouched-fields--field-string-boolean---void)
* [`setValues: (fields: { [field: string]: any }) => void`](#setvalues-fields--field-string-any---void)
* [`status?: any`](#status-any)
* [`touched: { [field: string]: boolean }`](#touched--field-string-boolean-)
* [`values: { [field: string]: any }`](#values--field-string-any-)
* [`validateForm: (values?: any) => void`](#validateform-values-any--void)
* [`component`](#component)
* [`render: (props: FormikProps<Values>) => ReactNode`](#render-props-formikpropsvalues--reactnode)
* [`children: func`](#children-func)
* [`enableReinitialize?: boolean`](#enablereinitialize-boolean)
* [`isInitialValid?: boolean`](#isinitialvalid-boolean)
* [`initialValues?: Values`](#initialvalues-values)
* [`onReset?: (values: Values, formikBag: FormikBag) => void`](#onreset-values-values-formikbag-formikbag--void)
* [`onSubmit: (values: Values, formikBag: FormikBag) => void`](#onsubmit-values-values-formikbag-formikbag--void)
* [`validate?: (values: Values) => FormikError<Values> | Promise<any>`](#validate-values-values--formikerrorvalues--promiseany)
* [`validateOnBlur?: boolean`](#validateonblur-boolean)
* [`validateOnChange?: boolean`](#validateonchange-boolean)
* [`validationSchema?: Schema | (() => Schema)`](#validationschema-schema----schema)
* [`<Field />`](#field-)
* [`validate?: (value: any) => undefined | string | Promise<any>`](#validate-value-any--undefined--string--promiseany)
* [`<FieldArray/>`](#fieldarray)
* [`name: string`](#name-string)
* [FieldArray Helpers](#fieldarray-helpers)
* [FieldArray render methods](#fieldarray-render-methods)
* [`render: (arrayHelpers: ArrayHelpers) => React.ReactNode`](#render-arrayhelpers-arrayhelpers--reactreactnode)
* [`component: React.ReactNode`](#component-reactreactnode)
* [`<Form />`](#form-)
* [`withFormik(options)`](#withformikoptions)
* [`options`](#options)
* [`displayName?: string`](#displayname-string)
* [`enableReinitialize?: boolean`](#enablereinitialize-boolean-1)
* [`handleSubmit: (values: Values, formikBag: FormikBag) => void`](#handlesubmit-values-values-formikbag-formikbag--void)
* [The "FormikBag":](#the-formikbag)
* [`isInitialValid?: boolean | (props: Props) => boolean`](#isinitialvalid-boolean--props-props--boolean)
* [`mapPropsToValues?: (props: Props) => Values`](#mappropstovalues-props-props--values)
* [`validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`](#validate-values-values-props-props--formikerrorvalues--promiseany)
* [`validateOnBlur?: boolean`](#validateonblur-boolean-1)
* [`validateOnChange?: boolean`](#validateonchange-boolean-1)
* [`validationSchema?: Schema | ((props: Props) => Schema)`](#validationschema-schema--props-props--schema)
* [Injected props and methods](#injected-props-and-methods)
* [Organizations and projects using Formik](#organizations-and-projects-using-formik)
* [Authors](#authors)
* [Contributors](#contributors)
- [Guides](#guides)
- [Basics](#basics)
- [React Native](#react-native)
- [Why use `setFieldValue` instead of `handleChange`?](#why-use-setfieldvalue-instead-of-handlechange)
- [Avoiding new functions in render](#avoiding-new-functions-in-render)
- [Using Formik with TypeScript](#using-formik-with-typescript)
- [Render props (`<Formik />` and `<Field/>`)](#render-props-formik--and-field)
- [`withFormik()`](#withformik)
- [API](#api)
- [`<Formik />`](#formik-)
- [Formik render methods](#formik-render-methods)
- [Formik props](#formik-props)
- [`dirty: boolean`](#dirty-boolean)
- [`errors: { [field: string]: string }`](#errors--field-string-string-)
- [`handleBlur: (e: any) => void`](#handleblur-e-any--void)
- [`handleChange: (e: React.ChangeEvent<any>) => void`](#handlechange-e-reactchangeeventany--void)
- [`handleReset: () => void`](#handlereset---void)
- [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
- [`isSubmitting: boolean`](#issubmitting-boolean)
- [`isValid: boolean`](#isvalid-boolean)
- [`resetForm: (nextValues?: Values) => void`](#resetform-nextvalues-values--void)
- [`setErrors: (fields: { [field: string]: string }) => void`](#seterrors-fields--field-string-string---void)
- [`setFieldError: (field: string, errorMsg: string) => void`](#setfielderror-field-string-errormsg-string--void)
- [`setFieldTouched: (field: string, isTouched: boolean, shouldValidate?: boolean) => void`](#setfieldtouched-field-string-istouched-boolean-shouldvalidate-boolean--void)
- [`setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void`](#setfieldvalue-field-string-value-any-shouldvalidate-boolean--void)
- [`setStatus: (status?: any) => void`](#setstatus-status-any--void)
- [`setSubmitting: (isSubmitting: boolean) => void`](#setsubmitting-issubmitting-boolean--void)
- [`setTouched: (fields: { [field: string]: boolean }) => void`](#settouched-fields--field-string-boolean---void)
- [`setValues: (fields: { [field: string]: any }) => void`](#setvalues-fields--field-string-any---void)
- [`status?: any`](#status-any)
- [`touched: { [field: string]: boolean }`](#touched--field-string-boolean-)
- [`values: { [field: string]: any }`](#values--field-string-any-)
- [`validateForm: (values?: any) => void`](#validateform-values-any--void)
- [`component`](#component)
- [`render: (props: FormikProps<Values>) => ReactNode`](#render-props-formikpropsvalues--reactnode)
- [`children: func`](#children-func)
- [`enableReinitialize?: boolean`](#enablereinitialize-boolean)
- [`isInitialValid?: boolean`](#isinitialvalid-boolean)
- [`initialValues?: Values`](#initialvalues-values)
- [`onReset?: (values: Values, formikBag: FormikBag) => void`](#onreset-values-values-formikbag-formikbag--void)
- [`onSubmit: (values: Values, formikBag: FormikBag) => void`](#onsubmit-values-values-formikbag-formikbag--void)
- [`validate?: (values: Values) => FormikError<Values> | Promise<any>`](#validate-values-values--formikerrorvalues--promiseany)
- [`validateOnBlur?: boolean`](#validateonblur-boolean)
- [`validateOnChange?: boolean`](#validateonchange-boolean)
- [`validationSchema?: Schema | (() => Schema)`](#validationschema-schema----schema)
- [`<Field />`](#field-)
- [`validate?: (value: any) => undefined | string | Promise<any>`](#validate-value-any--undefined--string--promiseany)
- [`<FieldArray/>`](#fieldarray)
- [`name: string`](#name-string)
- [`validateOnChange?: boolean`](#validateonchange-boolean-1)
- [FieldArray Validation Gotchas](#fieldarray-validation-gotchas)
- [FieldArray Helpers](#fieldarray-helpers)
- [FieldArray render methods](#fieldarray-render-methods)
- [`render: (arrayHelpers: ArrayHelpers) => React.ReactNode`](#render-arrayhelpers-arrayhelpers--reactreactnode)
- [`component: React.ReactNode`](#component-reactreactnode)
- [`<Form />`](#form-)
- [`withFormik(options)`](#withformikoptions)
- [`options`](#options)
- [`displayName?: string`](#displayname-string)
- [`enableReinitialize?: boolean`](#enablereinitialize-boolean-1)
- [`handleSubmit: (values: Values, formikBag: FormikBag) => void`](#handlesubmit-values-values-formikbag-formikbag--void)
- [The "FormikBag":](#the-formikbag)
- [`isInitialValid?: boolean | (props: Props) => boolean`](#isinitialvalid-boolean--props-props--boolean)
- [`mapPropsToValues?: (props: Props) => Values`](#mappropstovalues-props-props--values)
- [`validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`](#validate-values-values-props-props--formikerrorvalues--promiseany)
- [`validateOnBlur?: boolean`](#validateonblur-boolean-1)
- [`validateOnChange?: boolean`](#validateonchange-boolean-2)
- [`validationSchema?: Schema | ((props: Props) => Schema)`](#validationschema-schema--props-props--schema)
- [Injected props and methods](#injected-props-and-methods)
- [Organizations and projects using Formik](#organizations-and-projects-using-formik)
- [Authors](#authors)
- [Contributors](#contributors)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -1368,6 +1368,76 @@ export const FriendList = () => (

The name or path to the relevant key in [`values`].

##### `validateOnChange?: boolean`

Default is `true`. Determines if form validation should or should not be run _after_ any array manipulations.

#### FieldArray Validation Gotchas

Validation can be tricky with `<FieldArray>`.

If you use [`validationSchema`] and your form has array validation requirements (like a min length) as well as nested array field requirements, displaying errors can be tricky. Formik/Yup will show validation errors inside out. For example,

```js
const schema = Yup.object().shape({
friends: Yup.array()
.of(
Yup.object().shape({
name: Yup.string()
.min(4, 'too short')
.required('Required'), // these constraints take precedence
salary: Yup.string()
.min(3, 'cmon')
.required('Required'), // these constraints take precedence
})
)
.required('Must have friends') // these constraints are shown if and only if inner constraints are satisfied
.min(3, 'Minimum of 3 friends'),
});
```

Since Yup and your custom validation function should always output error messages as strings, you'll need to sniff whether your nested error is an array or a string when you go to display it.

So...to display `'Must have friends'` and `'Minimum of 3 friends'` (our example's array validation contstraints)...

**_Bad_**

```js
// within a `FieldArray`'s render
const FriendArrayErrors = errors =>
errors.friends ? <div>{errors.friends}</div> : null; // app will crash
```

**_Good_**

```js
// within a `FieldArray`'s render
const FriendArrayErrors = errors =>
typeof friends === 'string' ? <div>{errors.friends}</div> : null;
```

For the nested field errors, you should assume that no part of the object is defined unless you've checked for it. Thus, you may want to do yourself a favor and make a custom `<ErrorMessage />` component that looks like this:

```js
import { Field, getIn } from 'formik';
const ErrorMessage = ({ name }) => (
<Field
name={name}
render={({ form }) => {
const error = getIn(form.errors, name);
const touch = getIn(form.touched, name);
return touch && error ? error : null;
}}
/>
);
// Usage
<ErrorMessage name="friends[0].name" />; // => null, 'too short', or 'required'
```

_NOTE_: In Formik v0.12 / 1.0, a new `meta` prop may be be added to `Field` and `FieldArray` that will give you relevant metadata such as `error` & `touch`, which will save you from having to use Formik or lodash's getIn or checking if the path is defined on your own.

#### FieldArray Helpers

The following methods are made available via render props.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@types/lodash.topath": "4.5.3",
"@types/prop-types": "15.5.1",
"@types/react": "16.0.28",
"@types/react-dom": "^16.0.3",
"@types/react-native": "^0.52.8",
"@types/react-test-renderer": "15.5.2",
"@types/warning": "^3.0.0",
Expand Down
9 changes: 6 additions & 3 deletions src/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { dlv, isPromise } from './utils';
import { getIn, isPromise } from './utils';

import { FormikProps } from './formik';
import { isFunction, isEmptyChildren } from './utils';
Expand Down Expand Up @@ -48,7 +48,10 @@ export interface FieldConfig {
/**
* Field component to render. Can either be a string like 'select' or a component.
*/
component?: string | React.ComponentType<FieldProps<any>> | React.ComponentType<void>;
component?:
| string
| React.ComponentType<FieldProps<any>>
| React.ComponentType<void>;

/**
* Render prop (works like React router's <Route render={props =>} />)
Expand Down Expand Up @@ -167,7 +170,7 @@ export class Field<Props extends FieldAttributes = any> extends React.Component<
value:
props.type === 'radio' || props.type === 'checkbox'
? props.value // React uses checked={} for these inputs
: dlv(formik.values, name),
: getIn(formik.values, name),
name,
onChange: validate ? this.handleChange : formik.handleChange,
onBlur: validate ? this.handleBlur : formik.handleBlur,
Expand Down
56 changes: 38 additions & 18 deletions src/FieldArray.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { dlv, setDeep, isEmptyChildren } from './utils';
import { FormikProps, FormikState, isFunction } from './formik';
import { isEmptyChildren, getIn, setIn } from './utils';
import { SharedRenderProps } from './types';
import * as PropTypes from 'prop-types';
import { FormikProps, FormikState } from './formik';

export type FieldArrayConfig = {
/** Really the path to the array field to be updated */
name: string;
/** Should field array validate the form AFTER array updates/changes? */
validateOnChange?: boolean;
} & SharedRenderProps<ArrayHelpers & { form: FormikProps<any> }>;

export interface ArrayHelpers {
Expand Down Expand Up @@ -52,6 +54,9 @@ export const insert = (array: any[], index: number, value: any) => {
};

export class FieldArray extends React.Component<FieldArrayConfig, {}> {
static defaultProps = {
validateOnChange: true,
};
static contextTypes = {
formik: PropTypes.object,
};
Expand All @@ -68,18 +73,31 @@ export class FieldArray extends React.Component<FieldArrayConfig, {}> {
alterTouched: boolean,
alterErrors: boolean
) => {
const { setFormikState, values, touched, errors } = this.context.formik;
const { name } = this.props;
setFormikState((prevState: FormikState<any>) => ({
...prevState,
values: setDeep(name, fn(dlv(values, name)), prevState.values),
errors: alterErrors
? setDeep(name, fn(dlv(errors, name)), prevState.errors)
: prevState.errors,
touched: alterTouched
? setDeep(name, fn(dlv(touched, name)), prevState.touched)
: prevState.touched,
}));
const {
setFormikState,
validateForm,
values,
touched,
errors,
} = this.context.formik;
const { name, validateOnChange } = this.props;
setFormikState(
(prevState: FormikState<any>) => ({
...prevState,
values: setIn(prevState.values, name, fn(getIn(values, name))),
errors: alterErrors
? setIn(prevState.errors, name, fn(getIn(errors, name)))
: prevState.errors,
touched: alterTouched
? setIn(prevState.touched, name, fn(getIn(touched, name)))
: prevState.touched,
}),
() => {
if (validateOnChange) {
validateForm();
}
}
);
};

push = (value: any) =>
Expand Down Expand Up @@ -128,12 +146,14 @@ export class FieldArray extends React.Component<FieldArrayConfig, {}> {
let result: any;
this.updateArrayField(
// so this gets call 3 times
(array: any[]) => {
const copy = [...(array || [])];
(array?: any[]) => {
const copy = array ? [...array] : [];
if (!result) {
result = copy[index];
}
copy.splice(index, 1);
if (isFunction(copy.splice)) {
copy.splice(index, 1);
}
return copy;
},
true,
Expand Down
Loading

0 comments on commit 0d6b765

Please sign in to comment.