Skip to content

Commit d42cccd

Browse files
authoredJun 6, 2018
Fixed field-level validation bug (#32)
* Fixed field-level validation bug * Improved travis config
1 parent b775139 commit d42cccd

10 files changed

+6802
-5439
lines changed
 

‎.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ script:
1111
- npm start validate
1212
after_success:
1313
- npx codecov
14-
- npm install --global semantic-release
15-
# - semantic-release pre && npm publish && semantic-release post
1614
branches:
1715
only:
1816
- master

‎package-lock.json

Lines changed: 6647 additions & 5339 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,46 +25,48 @@
2525
},
2626
"homepage": "https://github.com/final-form/react-final-form-arrays#readme",
2727
"devDependencies": {
28-
"@types/react": "^16.0.38",
29-
"babel-eslint": "^8.2.2",
30-
"babel-jest": "^22.4.1",
28+
"@types/react": "^16.3.16",
29+
"babel-eslint": "^8.2.3",
30+
"babel-jest": "^23.0.1",
3131
"babel-plugin-external-helpers": "^6.22.0",
3232
"babel-plugin-transform-flow-strip-types": "^6.22.0",
33-
"babel-preset-env": "^1.6.1",
33+
"babel-preset-env": "^1.7.0",
3434
"babel-preset-react": "^6.24.1",
3535
"babel-preset-stage-2": "^6.24.1",
36-
"bundlesize": "^0.16.0",
36+
"bundlesize": "^0.17.0",
3737
"doctoc": "^1.3.1",
38-
"eslint": "^4.18.1",
38+
"eslint": "^4.19.1",
3939
"eslint-config-react-app": "^2.1.0",
40-
"eslint-plugin-babel": "^4.1.2",
41-
"eslint-plugin-flowtype": "^2.46.1",
42-
"eslint-plugin-import": "^2.9.0",
40+
"eslint-plugin-babel": "^5.1.0",
41+
"eslint-plugin-flowtype": "^2.49.3",
42+
"eslint-plugin-import": "^2.12.0",
4343
"eslint-plugin-jsx-a11y": "^6.0.2",
44-
"eslint-plugin-react": "^7.7.0",
45-
"final-form": "^4.2.1",
44+
"eslint-plugin-react": "^7.9.1",
45+
"final-form": "^4.7.3",
4646
"final-form-arrays": "^1.0.4",
47-
"flow-bin": "^0.66.0",
47+
"flow-bin": "^0.73.0",
48+
"glow": "^1.2.2",
4849
"husky": "^0.14.3",
49-
"jest": "^22.4.2",
50-
"lint-staged": "^7.0.0",
51-
"nps": "^5.7.1",
50+
"jest": "^23.1.0",
51+
"jest-watch-typeahead": "^0.1.0",
52+
"lint-staged": "^7.1.3",
53+
"nps": "^5.9.0",
5254
"nps-utils": "^1.5.0",
53-
"prettier": "^1.10.2",
55+
"prettier": "^1.12.0",
5456
"prettier-eslint-cli": "^4.7.1",
55-
"prop-types": "^15.6.0",
57+
"prop-types": "^15.6.1",
5658
"raf": "^3.4.0",
57-
"react": "^16.1.0",
58-
"react-dom": "^16.1.0",
59-
"react-final-form": "^3.1.0",
60-
"rollup": "^0.56.2",
61-
"rollup-plugin-babel": "^3.0.2",
62-
"rollup-plugin-commonjs": "^8.3.0",
59+
"react": "^16.4.0",
60+
"react-dom": "^16.4.0",
61+
"react-final-form": "^3.5.1",
62+
"rollup": "^0.60.0",
63+
"rollup-plugin-babel": "^3.0.4",
64+
"rollup-plugin-commonjs": "^9.1.3",
6365
"rollup-plugin-flow": "^1.1.1",
64-
"rollup-plugin-node-resolve": "^3.0.3",
66+
"rollup-plugin-node-resolve": "^3.3.0",
6567
"rollup-plugin-replace": "^2.0.0",
66-
"rollup-plugin-uglify": "^3.0.0",
67-
"typescript": "^2.7.2"
68+
"rollup-plugin-uglify": "^4.0.0",
69+
"typescript": "^2.9.1"
6870
},
6971
"peerDependencies": {
7072
"final-form": ">=4.0.0",
@@ -73,7 +75,10 @@
7375
"react": "^15.3.0 || ^16.0.0-0"
7476
},
7577
"jest": {
76-
"setupFiles": ["raf/polyfill"]
78+
"watchPlugins": [
79+
"jest-watch-typeahead/filename",
80+
"jest-watch-typeahead/testname"
81+
]
7782
},
7883
"lint-staged": {
7984
"*.{js*,ts*,json,md,css}": ["prettier --write", "git add"]
@@ -91,5 +96,9 @@
9196
"path": "dist/react-final-form-arrays.cjs.js",
9297
"threshold": "3kB"
9398
}
94-
]
99+
],
100+
"dependencies": {
101+
"npm-check": "^5.7.1",
102+
"react-lifecycles-compat": "^3.0.4"
103+
}
95104
}

‎rollup.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import resolve from 'rollup-plugin-node-resolve'
22
import babel from 'rollup-plugin-babel'
33
import flow from 'rollup-plugin-flow'
44
import commonjs from 'rollup-plugin-commonjs'
5-
import uglify from 'rollup-plugin-uglify'
5+
import { uglify } from 'rollup-plugin-uglify'
66
import replace from 'rollup-plugin-replace'
77

88
const minify = process.env.MINIFY
@@ -34,7 +34,6 @@ if (es) {
3434

3535
// eslint-disable-next-line no-nested-ternary
3636
export default {
37-
name: 'react-final-form-arrays',
3837
input: 'src/index.js',
3938
output: Object.assign(
4039
{

‎src/FieldArray.js

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
// @flow
22
import * as React from 'react'
3-
import warning from './warning'
3+
import { polyfill } from 'react-lifecycles-compat'
44
import PropTypes from 'prop-types'
5-
import { fieldSubscriptionItems, version as ffVersion } from 'final-form'
5+
import {
6+
fieldSubscriptionItems,
7+
version as ffVersion,
8+
ARRAY_ERROR
9+
} from 'final-form'
10+
import { version as rffVersion } from 'react-final-form'
611
import type { ReactContext } from 'react-final-form'
712
import diffSubscription from './diffSubscription'
8-
import type { FieldSubscription, FieldState } from 'final-form'
13+
import type { FieldSubscription, FieldState, FieldValidator } from 'final-form'
914
import type { Mutators } from 'final-form-arrays'
1015
import type { FieldArrayProps as Props } from './types'
1116
import renderComponent from './renderComponent'
12-
import { version } from './index'
17+
export const version = '1.0.4'
18+
19+
const versions = {
20+
'final-form': ffVersion,
21+
'react-final-form': rffVersion,
22+
'react-final-form-arrays': version
23+
}
1324

1425
const all: FieldSubscription = fieldSubscriptionItems.reduce((result, key) => {
1526
result[key] = true
1627
return result
1728
}, {})
1829

1930
type State = {
20-
state: FieldState
31+
state: ?FieldState
2132
}
2233

23-
export default class FieldArray extends React.PureComponent<Props, State> {
34+
class FieldArray extends React.PureComponent<Props, State> {
2435
context: ReactContext
2536
props: Props
2637
state: State
@@ -33,10 +44,12 @@ export default class FieldArray extends React.PureComponent<Props, State> {
3344
constructor(props: Props, context: ReactContext) {
3445
super(props, context)
3546
let initialState
36-
warning(
37-
context.reactFinalForm,
38-
'FieldArray must be used inside of a ReactFinalForm component'
39-
)
47+
// istanbul ignore next
48+
if (process.env.NODE_ENV !== 'production' && !context.reactFinalForm) {
49+
console.error(
50+
'Warning: FieldArray must be used inside of a ReactFinalForm component'
51+
)
52+
}
4053
const { reactFinalForm } = this.context
4154
if (reactFinalForm) {
4255
// avoid error, warning will alert developer to their mistake
@@ -48,7 +61,7 @@ export default class FieldArray extends React.PureComponent<Props, State> {
4861
}
4962
})
5063
}
51-
this.state = { state: initialState || {} }
64+
this.state = { state: initialState }
5265
this.bindMutators(props)
5366
this.mounted = false
5467
}
@@ -62,20 +75,36 @@ export default class FieldArray extends React.PureComponent<Props, State> {
6275
listener,
6376
subscription ? { ...subscription, length: true } : all,
6477
{
65-
getValidator: () => this.props.validate
78+
getValidator: () => this.validate
6679
}
6780
)
6881
}
6982

83+
validate: FieldValidator = (...args) => {
84+
const { validate } = this.props
85+
if (!validate) return undefined
86+
const error = validate(...args)
87+
if (!error || Array.isArray(error)) {
88+
return error
89+
} else {
90+
const arrayError = []
91+
// gross, but we have to set a string key on the array
92+
;((arrayError: any): Object)[ARRAY_ERROR] = error
93+
return arrayError
94+
}
95+
}
96+
7097
bindMutators = ({ name }: Props) => {
7198
const { reactFinalForm } = this.context
7299
if (reactFinalForm) {
73100
const { mutators } = reactFinalForm
74101
const hasMutators = !!(mutators && mutators.push && mutators.pop)
75-
warning(
76-
hasMutators,
77-
'Array mutators not found. You need to provide the mutators from final-form-arrays to your form'
78-
)
102+
// istanbul ignore next
103+
if (process.env.NODE_ENV !== 'production' && !hasMutators) {
104+
console.error(
105+
'Warning: Array mutators not found. You need to provide the mutators from final-form-arrays to your form'
106+
)
107+
}
79108
if (hasMutators) {
80109
this.mutators = Object.keys(mutators).reduce((result, key) => {
81110
result[key] = (...args) => mutators[key](name, ...args)
@@ -95,23 +124,27 @@ export default class FieldArray extends React.PureComponent<Props, State> {
95124

96125
forEach = (iterator: (name: string, index: number) => void): void => {
97126
const { name } = this.props
98-
const { length } = this.state.state
127+
// required || for Flow, but results in uncovered line in Jest/Istanbul
128+
// istanbul ignore next
129+
const length = this.state.state ? this.state.state.length || 0 : 0
99130
for (let i = 0; i < length; i++) {
100131
iterator(`${name}[${i}]`, i)
101132
}
102133
}
103134

104135
map = (iterator: (name: string, index: number) => any): any[] => {
105136
const { name } = this.props
106-
const { length } = this.state.state
137+
// required || for Flow, but results in uncovered line in Jest/Istanbul
138+
// istanbul ignore next
139+
const length = this.state.state ? this.state.state.length || 0 : 0
107140
const results: any[] = []
108141
for (let i = 0; i < length; i++) {
109142
results.push(iterator(`${name}[${i}]`, i))
110143
}
111144
return results
112145
}
113146

114-
componentWillReceiveProps(nextProps: Props) {
147+
UNSAFE_componentWillReceiveProps(nextProps: Props) {
115148
const { name, subscription } = nextProps
116149
if (
117150
this.props.name !== name ||
@@ -158,7 +191,8 @@ export default class FieldArray extends React.PureComponent<Props, State> {
158191
valid,
159192
visited,
160193
...fieldStateFunctions
161-
} = this.state.state
194+
} =
195+
this.state.state || {}
162196
const meta = {
163197
active,
164198
dirty,
@@ -188,7 +222,8 @@ export default class FieldArray extends React.PureComponent<Props, State> {
188222
...fieldState
189223
},
190224
meta,
191-
...rest
225+
...rest,
226+
__versions: versions
192227
},
193228
`FieldArray(${name})`
194229
)
@@ -198,3 +233,7 @@ export default class FieldArray extends React.PureComponent<Props, State> {
198233
FieldArray.contextTypes = {
199234
reactFinalForm: PropTypes.object
200235
}
236+
237+
polyfill(FieldArray)
238+
239+
export default FieldArray

‎src/FieldArray.test.js

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,48 @@ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
99

1010
describe('FieldArray', () => {
1111
it('should warn error if not used inside a form', () => {
12+
const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {})
1213
TestUtils.renderIntoDocument(
1314
<FieldArray name="foo" render={() => <div />} />
1415
)
16+
expect(spy).toHaveBeenCalled()
17+
expect(spy).toHaveBeenCalledTimes(1)
18+
expect(spy).toHaveBeenCalledWith(
19+
'Warning: FieldArray must be used inside of a ReactFinalForm component'
20+
)
21+
spy.mockRestore()
1522
})
1623

1724
it('should warn if no render strategy is provided', () => {
25+
const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {})
1826
TestUtils.renderIntoDocument(
1927
<Form
2028
onSubmit={onSubmitMock}
2129
mutators={arrayMutators}
2230
render={() => <FieldArray name="foo" />}
2331
/>
2432
)
33+
expect(spy).toHaveBeenCalled()
34+
expect(spy).toHaveBeenCalledTimes(1)
35+
expect(spy).toHaveBeenCalledWith(
36+
'Warning: Must specify either a render prop, a render function as children, or a component prop to FieldArray(foo)'
37+
)
38+
spy.mockRestore()
2539
})
2640

2741
it('should warn if no array mutators provided', () => {
42+
const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {})
2843
TestUtils.renderIntoDocument(
2944
<Form onSubmit={onSubmitMock}>
3045
{() => <FieldArray name="foo" render={() => <div />} />}
3146
</Form>
3247
)
48+
expect(spy).toHaveBeenCalled()
49+
expect(spy).toHaveBeenCalledTimes(1)
50+
expect(spy).toHaveBeenCalledWith(
51+
'Warning: Array mutators not found. You need to provide the mutators from final-form-arrays to your form'
52+
)
53+
spy.mockRestore()
3354
})
3455

3556
it('should render with a render component', () => {
@@ -53,6 +74,7 @@ describe('FieldArray', () => {
5374
<Form
5475
onSubmit={onSubmitMock}
5576
mutators={arrayMutators}
77+
subscription={{}}
5678
initialValues={{ dogs: ['Odie'], cats: ['Garfield'] }}
5779
>
5880
{() => (
@@ -129,7 +151,7 @@ describe('FieldArray', () => {
129151
<Form onSubmit={onSubmitMock} render={render} />
130152
)
131153
expect(render).toHaveBeenCalled()
132-
expect(render).toHaveBeenCalledTimes(2)
154+
expect(render).toHaveBeenCalledTimes(1)
133155
expect(renderArray).toHaveBeenCalled()
134156
expect(renderArray).toHaveBeenCalledTimes(1)
135157
})
@@ -152,13 +174,13 @@ describe('FieldArray', () => {
152174
/>
153175
)
154176
expect(render).toHaveBeenCalled()
155-
expect(render).toHaveBeenCalledTimes(2)
177+
expect(render).toHaveBeenCalledTimes(1)
156178
expect(renderArray).toHaveBeenCalled()
157-
expect(renderArray).toHaveBeenCalledTimes(2)
158-
expect(renderArray.mock.calls[1][0].meta.dirty).not.toBeUndefined()
159-
expect(renderArray.mock.calls[1][0].meta.dirty).toBe(false)
160-
expect(renderArray.mock.calls[1][0].fields.length).not.toBeUndefined()
161-
expect(renderArray.mock.calls[1][0].fields.length).toBe(2)
179+
expect(renderArray).toHaveBeenCalledTimes(1)
180+
expect(renderArray.mock.calls[0][0].meta.dirty).not.toBeUndefined()
181+
expect(renderArray.mock.calls[0][0].meta.dirty).toBe(false)
182+
expect(renderArray.mock.calls[0][0].fields.length).not.toBeUndefined()
183+
expect(renderArray.mock.calls[0][0].fields.length).toBe(2)
162184
})
163185

164186
it('should unsubscribe on unmount', () => {
@@ -213,7 +235,7 @@ describe('FieldArray', () => {
213235
/>
214236
)
215237
expect(render).toHaveBeenCalled()
216-
expect(render).toHaveBeenCalledTimes(2)
238+
expect(render).toHaveBeenCalledTimes(1)
217239
expect(renderArray).toHaveBeenCalled()
218240
expect(renderArray).toHaveBeenCalledTimes(1)
219241
expect(renderArray.mock.calls[0][0].meta.valid).toBe(true)
@@ -246,7 +268,7 @@ describe('FieldArray', () => {
246268
/>
247269
)
248270
expect(render).toHaveBeenCalled()
249-
expect(render).toHaveBeenCalledTimes(2)
271+
expect(render).toHaveBeenCalledTimes(1)
250272
expect(renderArray).toHaveBeenCalled()
251273
expect(renderArray).toHaveBeenCalledTimes(1)
252274

@@ -278,7 +300,7 @@ describe('FieldArray', () => {
278300
/>
279301
)
280302
expect(render).toHaveBeenCalled()
281-
expect(render).toHaveBeenCalledTimes(2)
303+
expect(render).toHaveBeenCalledTimes(1)
282304
expect(renderArray).toHaveBeenCalled()
283305
expect(renderArray).toHaveBeenCalledTimes(1)
284306

@@ -299,9 +321,10 @@ describe('FieldArray', () => {
299321
<Form
300322
onSubmit={onSubmitMock}
301323
mutators={arrayMutators}
324+
subscription={{}}
302325
render={() => (
303326
<form>
304-
<FieldArray name="foo">
327+
<FieldArray name="foo" subscription={{}}>
305328
{({ fields }) => (
306329
<div>
307330
{fields.map(name => (
@@ -336,10 +359,10 @@ describe('FieldArray', () => {
336359
TestUtils.Simulate.click(button)
337360
await sleep(2)
338361

339-
// notice it doesn't NEED to be called for foo[0] because that field hasn't changed!
340-
expect(renderInput).toHaveBeenCalledTimes(3)
341-
expect(renderInput.mock.calls[2][0].input.name).toBe('foo[1]')
342-
expect(renderInput.mock.calls[2][0].input.value).toBe('')
362+
// it must rerender foo[0] because the whole array is rerendered due to the change of length
363+
expect(renderInput).toHaveBeenCalledTimes(4)
364+
expect(renderInput.mock.calls[3][0].input.name).toBe('foo[1]')
365+
expect(renderInput.mock.calls[3][0].input.value).toBe('')
343366
})
344367

345368
it('should allow Fields to be rendered for complex objects', async () => {
@@ -349,9 +372,10 @@ describe('FieldArray', () => {
349372
<Form
350373
onSubmit={onSubmitMock}
351374
mutators={arrayMutators}
375+
subscription={{}}
352376
render={() => (
353377
<form>
354-
<FieldArray name="foo">
378+
<FieldArray name="foo" subscription={{}}>
355379
{({ fields }) => (
356380
<div>
357381
{fields.map(name => (
@@ -408,17 +432,17 @@ describe('FieldArray', () => {
408432
TestUtils.Simulate.click(button)
409433
await sleep(2)
410434

411-
// notice it doesn't NEED to be called for foo[0] because that field hasn't changed!
412-
expect(renderFirstNameInput).toHaveBeenCalledTimes(3)
413-
expect(renderFirstNameInput.mock.calls[2][0].input.name).toBe(
435+
// it must rerender foo[0] inputs because the whole array is rerendered due to the change of length
436+
expect(renderFirstNameInput).toHaveBeenCalledTimes(4)
437+
expect(renderFirstNameInput.mock.calls[3][0].input.name).toBe(
414438
'foo[1].firstName'
415439
)
416-
expect(renderFirstNameInput.mock.calls[2][0].input.value).toBe('')
417-
expect(renderLastNameInput).toHaveBeenCalledTimes(2)
418-
expect(renderLastNameInput.mock.calls[1][0].input.name).toBe(
440+
expect(renderFirstNameInput.mock.calls[3][0].input.value).toBe('')
441+
expect(renderLastNameInput).toHaveBeenCalledTimes(3)
442+
expect(renderLastNameInput.mock.calls[2][0].input.name).toBe(
419443
'foo[1].lastName'
420444
)
421-
expect(renderLastNameInput.mock.calls[1][0].input.value).toBe('')
445+
expect(renderLastNameInput.mock.calls[2][0].input.value).toBe('')
422446
})
423447

424448
it('should not warn if updating state after unmounting', async () => {

‎src/index.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
// @flow
2-
export { default as FieldArray } from './FieldArray'
3-
4-
export const version = '1.0.3'
2+
export { default as FieldArray, version } from './FieldArray'

‎src/renderComponent.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
22
import * as React from 'react'
3-
import warning from './warning'
43
import type { RenderableProps } from './types'
54

65
// shared logic between components that use either render prop,
@@ -16,14 +15,14 @@ export default function renderComponent<T>(
1615
if (render) {
1716
return render({ ...rest, children }) // inject children back in
1817
}
18+
// istanbul ignore next
1919
if (typeof children !== 'function') {
20-
warning(
21-
false,
22-
`Must specify either a render prop, a render function as children, or a component prop to ${
23-
name
24-
}`
25-
)
26-
return null // warning will alert developer to their mistake
20+
if (process.env.NODE_ENV !== 'production') {
21+
console.error(
22+
`Warning: Must specify either a render prop, a render function as children, or a component prop to ${name}`
23+
)
24+
return null // warning will alert developer to their mistake
25+
}
2726
}
2827
return children(rest)
2928
}

‎src/types.js.flow

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ export type FieldArrayRenderProps = {
3434
}
3535

3636
export type RenderableProps<T> = $Shape<{
37-
children: ((props: T) => React.Node) | React.Node,
37+
children: (props: T) => React.Node,
3838
component: React.ComponentType<*>,
3939
render: (props: T) => React.Node
4040
}>
4141

4242
export type FieldArrayProps = {
4343
name: string,
4444
subscription?: FieldSubscription,
45+
isEqual?: (any, any) => boolean,
4546
validate?: (value: ?(any[]), allValues: Object) => ?any
4647
} & RenderableProps<FieldArrayRenderProps>

‎src/warning.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.