diff --git a/README.md b/README.md index f1fbbc1..410f74d 100644 --- a/README.md +++ b/README.md @@ -81,45 +81,50 @@ const MyForm = () => ( -* [Examples](#examples) - * [Simple Example](#simple-example) -* [Rendering](#rendering) -* [API](#api) - * [`FieldArray : React.ComponentType`](#fieldarray--reactcomponenttypefieldarrayprops) - * [`version: string`](#version-string) -* [Types](#types) - * [`FieldArrayProps`](#fieldarrayprops) - * [`children?: ((props: FieldArrayRenderProps) => React.Node) | React.Node`](#children-props-fieldarrayrenderprops--reactnode--reactnode) - * [`component?: React.ComponentType`](#component-reactcomponenttypefieldarrayrenderprops) - * [`name: string`](#name-string) - * [`render?: (props: FieldArrayRenderProps) => React.Node`](#render-props-fieldarrayrenderprops--reactnode) - * [`subscription?: FieldSubscription`](#subscription-fieldsubscription) - * [`validate?: (value: ?any[], allValues: Object) => ?any`](#validate-value-any-allvalues-object--any) - * [`FieldArrayRenderProps`](#fieldarrayrenderprops) - * [`fields.forEach: (iterator: (name: string, index: number) => void) => void`](#fieldsforeach-iterator-name-string-index-number--void--void) - * [`fields.insert: (index: number, value: any) => void`](#fieldsinsert-index-number-value-any--void) - * [`fields.map: (iterator: (name: string, index: number) => any) => any[]`](#fieldsmap-iterator-name-string-index-number--any--any) - * [`fields.move: (from: number, to: number) => void`](#fieldsmove-from-number-to-number--void) - * [`fields.name: string`](#fieldsname-string) - * [`fields.pop: () => any`](#fieldspop---any) - * [`fields.push: (value: any) => void`](#fieldspush-value-any--void) - * [`fields.remove: (index: number) => any`](#fieldsremove-index-number--any) - * [`fields.shift: () => any`](#fieldsshift---any) - * [`fields.swap: (indexA: number, indexB: number) => void`](#fieldsswap-indexa-number-indexb-number--void) - * [`fields.unshift: (value: any) => void`](#fieldsunshift-value-any--void) - * [`meta.active?: boolean`](#metaactive-boolean) - * [`meta.data: Object`](#metadata-object) - * [`meta.dirty?: boolean`](#metadirty-boolean) - * [`meta.error?: any`](#metaerror-any) - * [`meta.initial?: any`](#metainitial-any) - * [`meta.invalid?: boolean`](#metainvalid-boolean) - * [`meta.pristine?: boolean`](#metapristine-boolean) - * [`meta.submitError?: any`](#metasubmiterror-any) - * [`meta.submitFailed?: boolean`](#metasubmitfailed-boolean) - * [`meta.submitSucceeded?: boolean`](#metasubmitsucceeded-boolean) - * [`meta.touched?: boolean`](#metatouched-boolean) - * [`meta.valid?: boolean`](#metavalid-boolean) - * [`meta.visited?: boolean`](#metavisited-boolean) +- [🏁 React Final Form Arrays](#-react-final-form-arrays) + - [Installation](#installation) + - [Usage](#usage) + - [Table of Contents](#table-of-contents) + - [Examples](#examples) + - [Simple Example](#simple-example) + - [Rendering](#rendering) + - [API](#api) + - [`FieldArray : React.ComponentType`](#fieldarray--reactcomponenttypefieldarrayprops) + - [`version: string`](#version-string) + - [Types](#types) + - [`FieldArrayProps`](#fieldarrayprops) + - [`children?: ((props: FieldArrayRenderProps) => React.Node) | React.Node`](#children-props-fieldarrayrenderprops--reactnode--reactnode) + - [`component?: React.ComponentType`](#component-reactcomponenttypefieldarrayrenderprops) + - [`name: string`](#name-string) + - [`render?: (props: FieldArrayRenderProps) => React.Node`](#render-props-fieldarrayrenderprops--reactnode) + - [`isEqual?: (allPreviousValues: Array, allNewValues: Array) => boolean`](#isequal-allpreviousvalues-arrayany-allnewvalues-arrayany--boolean) + - [`subscription?: FieldSubscription`](#subscription-fieldsubscription) + - [`validate?: (value: ?any[], allValues: Object) => ?any`](#validate-value-any-allvalues-object--any) + - [`FieldArrayRenderProps`](#fieldarrayrenderprops) + - [`fields.forEach: (iterator: (name: string, index: number) => void) => void`](#fieldsforeach-iterator-name-string-index-number--void--void) + - [`fields.insert: (index: number, value: any) => void`](#fieldsinsert-index-number-value-any--void) + - [`fields.map: (iterator: (name: string, index: number) => any) => any[]`](#fieldsmap-iterator-name-string-index-number--any--any) + - [`fields.move: (from: number, to: number) => void`](#fieldsmove-from-number-to-number--void) + - [`fields.name: string`](#fieldsname-string) + - [`fields.pop: () => any`](#fieldspop---any) + - [`fields.push: (value: any) => void`](#fieldspush-value-any--void) + - [`fields.remove: (index: number) => any`](#fieldsremove-index-number--any) + - [`fields.shift: () => any`](#fieldsshift---any) + - [`fields.swap: (indexA: number, indexB: number) => void`](#fieldsswap-indexa-number-indexb-number--void) + - [`fields.unshift: (value: any) => void`](#fieldsunshift-value-any--void) + - [`meta.active?: boolean`](#metaactive-boolean) + - [`meta.data: Object`](#metadata-object) + - [`meta.dirty?: boolean`](#metadirty-boolean) + - [`meta.error?: any`](#metaerror-any) + - [`meta.initial?: any`](#metainitial-any) + - [`meta.invalid?: boolean`](#metainvalid-boolean) + - [`meta.pristine?: boolean`](#metapristine-boolean) + - [`meta.submitError?: any`](#metasubmiterror-any) + - [`meta.submitFailed?: boolean`](#metasubmitfailed-boolean) + - [`meta.submitSucceeded?: boolean`](#metasubmitsucceeded-boolean) + - [`meta.touched?: boolean`](#metatouched-boolean) + - [`meta.valid?: boolean`](#metavalid-boolean) + - [`meta.visited?: boolean`](#metavisited-boolean) @@ -184,6 +189,10 @@ A render function that is given [`FieldArrayRenderProps`](#fieldarrayrenderprops), as well as any non-API props passed into the `` component. +#### `isEqual?: (allPreviousValues: Array, allNewValues: Array) => boolean` + +A function that can be used to compare two arrays of values (before and after every change) and calculate pristine/dirty checks. + #### `subscription?: FieldSubscription` A diff --git a/src/FieldArray.js b/src/FieldArray.js index 153b52b..eaa84c9 100644 --- a/src/FieldArray.js +++ b/src/FieldArray.js @@ -66,6 +66,14 @@ class FieldArray extends React.PureComponent { this.mounted = false } + isEqual = (a: Array, b: Array) => { + if (typeof this.props.isEqual === 'function') { + return this.props.isEqual(a, b) + } + + return true + } + subscribe = ( { name, subscription }: Props, listener: (state: FieldState) => void @@ -75,7 +83,8 @@ class FieldArray extends React.PureComponent { listener, subscription ? { ...subscription, length: true } : all, { - getValidator: () => this.validate + getValidator: () => this.validate, + isEqual: this.isEqual } ) } @@ -83,7 +92,7 @@ class FieldArray extends React.PureComponent { validate: FieldValidator = (...args) => { const { validate } = this.props if (!validate) return undefined - const error = validate(...args) + const error = validate(args[0], args[1]) if (!error || Array.isArray(error)) { return error } else { @@ -191,8 +200,7 @@ class FieldArray extends React.PureComponent { valid, visited, ...fieldStateFunctions - } = - this.state.state || {} + } = this.state.state || {} const meta = { active, dirty, diff --git a/src/FieldArray.test.js b/src/FieldArray.test.js index 1c10ba5..d42c71b 100644 --- a/src/FieldArray.test.js +++ b/src/FieldArray.test.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Fragment } from 'react' import TestUtils from 'react-dom/test-utils' import { Form, Field } from 'react-final-form' import arrayMutators from 'final-form-arrays' @@ -505,4 +505,48 @@ describe('FieldArray', () => { await sleep(2) expect(spy).not.toHaveBeenCalled() }) + + it('should provide default isEqual method to calculate pristine correctly', () => { + const arrayFieldRender = jest.fn(({ fields }) => ( + + {fields.map((name, index) => ( + + + + + ))} + + )) + + const formRender = jest.fn(() => ( + + )) + const dom = TestUtils.renderIntoDocument( +
+ ) + const input = TestUtils.findRenderedDOMComponentWithTag(dom, 'input') + const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') + + // initially pristine true + expect(formRender.mock.calls[0][0]).toMatchObject({ + pristine: true + }) + + // changing value, pristine false + TestUtils.Simulate.change(input, { target: { value: 'foo' } }) + expect(formRender.mock.calls[1][0]).toMatchObject({ pristine: false }) + + // changing value back to default, pristine true + TestUtils.Simulate.change(input, { target: { value: 'example' } }) + expect(formRender.mock.calls[2][0]).toMatchObject({ pristine: true }) + + // removing field, pristine false + TestUtils.Simulate.click(button) + expect(formRender.mock.calls[3][0]).toMatchObject({ pristine: false }) + }) })