From 0d6b765d04bf5efd352e6f2e4ed098bde4626639 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Thu, 8 Feb 2018 16:41:42 -0800 Subject: [PATCH] #417 Tweak validation within FieldArray, rename dlv to getIn (#419) * #417 Tweak validation within FieldArray, rename dlv to getIn * Update TOC --- README.md | 214 +++++++++++++++++++++++++++++--------------- package.json | 1 + src/Field.tsx | 9 +- src/FieldArray.tsx | 56 ++++++++---- src/formik.tsx | 17 ++-- src/utils.ts | 10 +-- test/utils.test.tsx | 88 +++--------------- yarn.lock | 7 ++ 8 files changed, 219 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index 98e59e6e5..231f859b3 100644 --- a/README.md +++ b/README.md @@ -306,81 +306,81 @@ npm install yup --save ** Table of Contents ** - - -* [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 (`` and ``)](#render-props-formik--and-field) - * [`withFormik()`](#withformik) -* [API](#api) - * [``](#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) => void`](#handlechange-e-reactchangeeventany--void) - * [`handleReset: () => void`](#handlereset---void) - * [`handleSubmit: (e: React.FormEvent) => 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) => 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 | Promise`](#validate-values-values--formikerrorvalues--promiseany) - * [`validateOnBlur?: boolean`](#validateonblur-boolean) - * [`validateOnChange?: boolean`](#validateonchange-boolean) - * [`validationSchema?: Schema | (() => Schema)`](#validationschema-schema----schema) - * [``](#field-) - * [`validate?: (value: any) => undefined | string | Promise`](#validate-value-any--undefined--string--promiseany) - * [``](#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-) - * [`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 | Promise`](#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 (`` and ``)](#render-props-formik--and-field) + - [`withFormik()`](#withformik) +- [API](#api) + - [``](#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) => void`](#handlechange-e-reactchangeeventany--void) + - [`handleReset: () => void`](#handlereset---void) + - [`handleSubmit: (e: React.FormEvent) => 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) => 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 | Promise`](#validate-values-values--formikerrorvalues--promiseany) + - [`validateOnBlur?: boolean`](#validateonblur-boolean) + - [`validateOnChange?: boolean`](#validateonchange-boolean) + - [`validationSchema?: Schema | (() => Schema)`](#validationschema-schema----schema) + - [``](#field-) + - [`validate?: (value: any) => undefined | string | Promise`](#validate-value-any--undefined--string--promiseany) + - [``](#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-) + - [`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 | Promise`](#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) @@ -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 ``. + +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 ?
{errors.friends}
: null; // app will crash +``` + +**_Good_** + +```js +// within a `FieldArray`'s render +const FriendArrayErrors = errors => + typeof friends === 'string' ?
{errors.friends}
: 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 `` component that looks like this: + +```js +import { Field, getIn } from 'formik'; + +const ErrorMessage = ({ name }) => ( + { + const error = getIn(form.errors, name); + const touch = getIn(form.touched, name); + return touch && error ? error : null; + }} + /> +); + +// Usage +; // => 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. diff --git a/package.json b/package.json index 84b4a5c64..5281440c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Field.tsx b/src/Field.tsx index 1c73b85d9..7be4c5ae4 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -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'; @@ -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> | React.ComponentType; + component?: + | string + | React.ComponentType> + | React.ComponentType; /** * Render prop (works like React router's } />) @@ -167,7 +170,7 @@ export class Field 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, diff --git a/src/FieldArray.tsx b/src/FieldArray.tsx index 8ed726b3e..05b3883de 100644 --- a/src/FieldArray.tsx +++ b/src/FieldArray.tsx @@ -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 }>; export interface ArrayHelpers { @@ -52,6 +54,9 @@ export const insert = (array: any[], index: number, value: any) => { }; export class FieldArray extends React.Component { + static defaultProps = { + validateOnChange: true, + }; static contextTypes = { formik: PropTypes.object, }; @@ -68,18 +73,31 @@ export class FieldArray extends React.Component { alterTouched: boolean, alterErrors: boolean ) => { - const { setFormikState, values, touched, errors } = this.context.formik; - const { name } = this.props; - setFormikState((prevState: FormikState) => ({ - ...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) => ({ + ...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) => @@ -128,12 +146,14 @@ export class FieldArray extends React.Component { 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, diff --git a/src/formik.tsx b/src/formik.tsx index 521fe60d1..b72e6e475 100644 --- a/src/formik.tsx +++ b/src/formik.tsx @@ -8,7 +8,7 @@ import { isPromise, isReactNative, isEmptyChildren, - setDeep, + setIn, setNestedObjectValues, } from './utils'; @@ -437,11 +437,11 @@ export class Formik extends React.Component< // Set form fields by name this.setState(prevState => ({ ...prevState, - values: setDeep(field, val, prevState.values), + values: setIn(prevState.values, field, val), })); if (this.props.validateOnChange) { - this.runValidations(setDeep(field, val, this.state.values)); + this.runValidations(setIn(this.state.values, field, val)); } }; @@ -454,7 +454,7 @@ export class Formik extends React.Component< this.setState( prevState => ({ ...prevState, - values: setDeep(field, value, prevState.values), + values: setIn(prevState.values, field, value), }), () => { if (this.props.validateOnChange && shouldValidate) { @@ -538,7 +538,7 @@ export class Formik extends React.Component< } this.setState(prevState => ({ - touched: setDeep(field, true, prevState.touched), + touched: setIn(prevState.touched, field, true), })); if (this.props.validateOnBlur) { @@ -555,7 +555,7 @@ export class Formik extends React.Component< this.setState( prevState => ({ ...prevState, - touched: setDeep(field, touched, prevState.touched), + touched: setIn(prevState.touched, field, touched), }), () => { if (this.props.validateOnBlur && shouldValidate) { @@ -569,7 +569,7 @@ export class Formik extends React.Component< // Set form field by name this.setState(prevState => ({ ...prevState, - errors: setDeep(field, message, prevState.errors), + errors: setIn(prevState.errors, field, message), })); }; @@ -695,7 +695,7 @@ export function yupToFormErrors(yupError: any): FormikErrors { let errors: any = {} as FormikErrors; for (let err of yupError.inner) { if (!errors[err.path]) { - errors = setDeep(err.path, err.message, errors); + errors = setIn(errors, err.path, err.message); } } return errors; @@ -724,3 +724,4 @@ export * from './Field'; export * from './Form'; export * from './withFormik'; export * from './FieldArray'; +export * from './utils'; diff --git a/src/utils.ts b/src/utils.ts index a4ad6b909..72652df27 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,9 +3,9 @@ import toPath from 'lodash.topath'; import cloneDeep from 'lodash.clonedeep'; /** - * @private Deeply get a value from an object via it's path. + * Deeply get a value from an object via it's path. */ -export function dlv( +export function getIn( obj: any, key: string | string[], def?: any, @@ -19,10 +19,10 @@ export function dlv( } /** - * @private Deeply set a value from in object via it's path. - * See https://github.com/developit/linkstate + * Deeply set a value from in object via it's path. + * @see https://github.com/developit/linkstate */ -export function setDeep(path: string, value: any, obj: any): any { +export function setIn(obj: any, path: string, value: any): any { let res: any = {}; let resVal: any = res; let i = 0; diff --git a/test/utils.test.tsx b/test/utils.test.tsx index a493ed8e4..bb4c98b0b 100644 --- a/test/utils.test.tsx +++ b/test/utils.test.tsx @@ -1,4 +1,4 @@ -import { setDeep, setNestedObjectValues } from '../src/utils'; +import { setIn, setNestedObjectValues } from '../src/utils'; describe('utils', () => { describe('setNestedObjectValues', () => { @@ -126,31 +126,31 @@ describe('utils', () => { }); }); - describe('setDeep', () => { + describe('setIn', () => { it('sets flat value', () => { const obj = { x: 'y' }; - const newObj = setDeep('flat', 'value', obj); + const newObj = setIn(obj, 'flat', 'value'); expect(obj).toEqual({ x: 'y' }); expect(newObj).toEqual({ x: 'y', flat: 'value' }); }); it('sets nested value', () => { const obj = { x: 'y' }; - const newObj = setDeep('nested.value', 'nested value', obj); + const newObj = setIn(obj, 'nested.value', 'nested value'); expect(obj).toEqual({ x: 'y' }); expect(newObj).toEqual({ x: 'y', nested: { value: 'nested value' } }); }); it('updates nested value', () => { const obj = { x: 'y', nested: { value: 'a' } }; - const newObj = setDeep('nested.value', 'b', obj); + const newObj = setIn(obj, 'nested.value', 'b'); expect(obj).toEqual({ x: 'y', nested: { value: 'a' } }); expect(newObj).toEqual({ x: 'y', nested: { value: 'b' } }); }); it('updates deep nested value', () => { const obj = { x: 'y', twofoldly: { nested: { value: 'a' } } }; - const newObj = setDeep('twofoldly.nested.value', 'b', obj); + const newObj = setIn(obj, 'twofoldly.nested.value', 'b'); expect(obj.twofoldly.nested === newObj.twofoldly.nested).toEqual(false); // fails, same object still expect(obj).toEqual({ x: 'y', twofoldly: { nested: { value: 'a' } } }); // fails, it's b here, too expect(newObj).toEqual({ x: 'y', twofoldly: { nested: { value: 'b' } } }); // works ofc @@ -158,101 +158,35 @@ describe('utils', () => { it('sets new array', () => { const obj = { x: 'y' }; - const newObj = setDeep('nested.0', 'value', obj); + const newObj = setIn(obj, 'nested.0', 'value'); expect(obj).toEqual({ x: 'y' }); expect(newObj).toEqual({ x: 'y', nested: ['value'] }); }); it('updates nested array value', () => { const obj = { x: 'y', nested: ['a'] }; - const newObj = setDeep('nested[0]', 'b', obj); + const newObj = setIn(obj, 'nested[0]', 'b'); expect(obj).toEqual({ x: 'y', nested: ['a'] }); expect(newObj).toEqual({ x: 'y', nested: ['b'] }); }); it('adds new item to nested array', () => { const obj = { x: 'y', nested: ['a'] }; - const newObj = setDeep('nested.1', 'b', obj); + const newObj = setIn(obj, 'nested.1', 'b'); expect(obj).toEqual({ x: 'y', nested: ['a'] }); expect(newObj).toEqual({ x: 'y', nested: ['a', 'b'] }); }); it('sticks to object with int key when defined', () => { const obj = { x: 'y', nested: { 0: 'a' } }; - const newObj = setDeep('nested.0', 'b', obj); + const newObj = setIn(obj, 'nested.0', 'b'); expect(obj).toEqual({ x: 'y', nested: { 0: 'a' } }); expect(newObj).toEqual({ x: 'y', nested: { 0: 'b' } }); }); it('supports bracket path', () => { const obj = { x: 'y' }; - const newObj = setDeep('nested[0]', 'value', obj); - expect(obj).toEqual({ x: 'y' }); - expect(newObj).toEqual({ x: 'y', nested: ['value'] }); - }); - }); - - describe('setDeep', () => { - it('sets flat value', () => { - const obj = { x: 'y' }; - const newObj = setDeep('flat', 'value', obj); - expect(obj).toEqual({ x: 'y' }); - expect(newObj).toEqual({ x: 'y', flat: 'value' }); - }); - - it('sets nested value', () => { - const obj = { x: 'y' }; - const newObj = setDeep('nested.value', 'nested value', obj); - expect(obj).toEqual({ x: 'y' }); - expect(newObj).toEqual({ x: 'y', nested: { value: 'nested value' } }); - }); - - it('updates nested value', () => { - const obj = { x: 'y', nested: { value: 'a' } }; - const newObj = setDeep('nested.value', 'b', obj); - expect(obj).toEqual({ x: 'y', nested: { value: 'a' } }); - expect(newObj).toEqual({ x: 'y', nested: { value: 'b' } }); - }); - - it('updates deep nested value', () => { - const obj = { x: 'y', twofoldly: { nested: { value: 'a' } } }; - const newObj = setDeep('twofoldly.nested.value', 'b', obj); - expect(obj.twofoldly.nested === newObj.twofoldly.nested).toEqual(false); // fails, same object still - expect(obj).toEqual({ x: 'y', twofoldly: { nested: { value: 'a' } } }); // fails, it's b here, too - expect(newObj).toEqual({ x: 'y', twofoldly: { nested: { value: 'b' } } }); // works ofc - }); - - it('sets new array', () => { - const obj = { x: 'y' }; - const newObj = setDeep('nested.0', 'value', obj); - expect(obj).toEqual({ x: 'y' }); - expect(newObj).toEqual({ x: 'y', nested: ['value'] }); - }); - - it('updates nested array value', () => { - const obj = { x: 'y', nested: ['a'] }; - const newObj = setDeep('nested[0]', 'b', obj); - expect(obj).toEqual({ x: 'y', nested: ['a'] }); - expect(newObj).toEqual({ x: 'y', nested: ['b'] }); - }); - - it('adds new item to nested array', () => { - const obj = { x: 'y', nested: ['a'] }; - const newObj = setDeep('nested.1', 'b', obj); - expect(obj).toEqual({ x: 'y', nested: ['a'] }); - expect(newObj).toEqual({ x: 'y', nested: ['a', 'b'] }); - }); - - it('sticks to object with int key when defined', () => { - const obj = { x: 'y', nested: { 0: 'a' } }; - const newObj = setDeep('nested.0', 'b', obj); - expect(obj).toEqual({ x: 'y', nested: { 0: 'a' } }); - expect(newObj).toEqual({ x: 'y', nested: { 0: 'b' } }); - }); - - it('supports bracket path', () => { - const obj = { x: 'y' }; - const newObj = setDeep('nested[0]', 'value', obj); + const newObj = setIn(obj, 'nested[0]', 'value'); expect(obj).toEqual({ x: 'y' }); expect(newObj).toEqual({ x: 'y', nested: ['value'] }); }); diff --git a/yarn.lock b/yarn.lock index 20d894330..216f8428a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,13 @@ version "15.5.1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.1.tgz#1ecf52621299e65b855374337fb11fd2d1066fc1" +"@types/react-dom@^16.0.3": + version "16.0.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" + dependencies: + "@types/node" "*" + "@types/react" "*" + "@types/react-native@^0.52.8": version "0.52.8" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.52.8.tgz#dd5aa7c4eb944d7c0b2b249a2bb811aa119f49c6"