Skip to content

Commit b3e2be5

Browse files
committed
Improve useFieldArray to allow async validation
also add more tests
1 parent 9f231a9 commit b3e2be5

2 files changed

Lines changed: 166 additions & 30 deletions

File tree

src/useFieldArray.test.tsx

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as React from 'react'
2-
import { act, render, cleanup } from '@testing-library/react'
2+
import { act, render, cleanup, waitFor } from '@testing-library/react'
33
import '@testing-library/jest-dom'
44
import arrayMutators from 'final-form-arrays'
55
import { ErrorBoundary } from './testUtils'
6-
import { Form, useField } from 'react-final-form'
6+
import { Form, useField, useFormState } from 'react-final-form'
77
import useFieldArray from './useFieldArray'
8+
import { ARRAY_ERROR } from 'final-form'
89

910
const onSubmitMock = (values: any) => {}
1011

@@ -41,7 +42,11 @@ describe('FieldArray', () => {
4142
return null
4243
}
4344
render(
44-
<Form onSubmit={onSubmitMock} mutators={arrayMutators as any} subscription={{}}>
45+
<Form
46+
onSubmit={onSubmitMock}
47+
mutators={arrayMutators as any}
48+
subscription={{}}
49+
>
4550
{() => (
4651
<form>
4752
<MyFieldArray />
@@ -65,9 +70,9 @@ describe('FieldArray', () => {
6570
// undefined is passed instead of a no-op function that always returns undefined.
6671
// This prevents final-form from tracking this field as having a validator,
6772
// which would trigger unnecessary form-wide validation.
68-
73+
6974
const useFieldSpy = jest.spyOn(require('react-final-form'), 'useField')
70-
75+
7176
const MyFieldArray = () => {
7277
const fieldArray = useFieldArray('names')
7378
return null
@@ -89,17 +94,17 @@ describe('FieldArray', () => {
8994

9095
// Verify that useField was called with validate: undefined
9196
const useFieldCalls = useFieldSpy.mock.calls
92-
const relevantCall = useFieldCalls.find(call => call[0] === 'names')
97+
const relevantCall = useFieldCalls.find((call) => call[0] === 'names')
9398
expect(relevantCall).toBeDefined()
9499
expect(relevantCall![1].validate).toBeUndefined()
95-
100+
96101
useFieldSpy.mockRestore()
97102
})
98103

99104
it('should call validator when validate prop is provided', () => {
100105
const fieldValidate = jest.fn(() => undefined)
101106
const fieldArraySpy = jest.fn()
102-
107+
103108
const MyFieldArray = () => {
104109
const fieldArray = useFieldArray('names', { validate: fieldValidate })
105110
fieldArraySpy(fieldArray)
@@ -126,11 +131,115 @@ describe('FieldArray', () => {
126131

127132
// Get the last call before mutations
128133
const lastCallBeforeMutations = fieldArraySpy.mock.calls.length - 1
129-
134+
130135
// Push an item to trigger validation again
131-
act(() => fieldArraySpy.mock.calls[lastCallBeforeMutations][0].fields.push('alice'))
136+
act(() =>
137+
fieldArraySpy.mock.calls[lastCallBeforeMutations][0].fields.push('alice')
138+
)
132139

133140
// Field validation should be called again after mutation
134141
expect(fieldValidate.mock.calls.length).toBeGreaterThan(initialCalls)
135142
})
143+
144+
it('should handle array errors', () => {
145+
const spy = jest.fn()
146+
const MyFieldArray = () => {
147+
spy(useFieldArray('names', { validate: (values) => ['required'] }))
148+
return null
149+
}
150+
render(
151+
<Form
152+
onSubmit={onSubmitMock}
153+
mutators={arrayMutators as any}
154+
subscription={{}}
155+
>
156+
{() => (
157+
<form>
158+
<MyFieldArray />
159+
</form>
160+
)}
161+
</Form>
162+
)
163+
164+
expect(spy).toHaveBeenCalledWith(
165+
expect.objectContaining({
166+
meta: expect.objectContaining({
167+
error: ['required']
168+
})
169+
})
170+
)
171+
})
172+
173+
it('should handle string error', () => {
174+
const spy = jest.fn()
175+
const spyState = jest.fn()
176+
const MyFieldArray = () => {
177+
spy(useFieldArray('names', { validate: (values) => 'failed' }))
178+
return null
179+
}
180+
const Debug = () => {
181+
spyState(useFormState().errors)
182+
return null
183+
}
184+
render(
185+
<Form
186+
onSubmit={onSubmitMock}
187+
mutators={arrayMutators as any}
188+
subscription={{}}
189+
>
190+
{() => (
191+
<form>
192+
<MyFieldArray />
193+
<Debug />
194+
</form>
195+
)}
196+
</Form>
197+
)
198+
199+
expect(spy).toHaveBeenCalledWith(
200+
expect.objectContaining({
201+
meta: expect.objectContaining({
202+
error: 'failed'
203+
})
204+
})
205+
)
206+
const expected: any[] = []
207+
;(expected as any)[ARRAY_ERROR] = 'failed'
208+
209+
expect(spyState).toHaveBeenCalledWith({ names: expected })
210+
})
211+
it('should handle Promises errors', async () => {
212+
const spy = jest.fn()
213+
const MyFieldArray = () => {
214+
spy(
215+
useFieldArray('names', {
216+
validate: (values) => Promise.resolve(['await fail'])
217+
})
218+
)
219+
return null
220+
}
221+
render(
222+
<Form
223+
onSubmit={onSubmitMock}
224+
mutators={arrayMutators as any}
225+
subscription={{}}
226+
>
227+
{() => (
228+
<form>
229+
<MyFieldArray />
230+
</form>
231+
)}
232+
</Form>
233+
)
234+
235+
waitFor(() =>
236+
expect(spy).toHaveBeenCalledWith(
237+
expect.objectContaining({
238+
meta: expect.objectContaining({
239+
error: ['await fail']
240+
})
241+
})
242+
)
243+
)
244+
})
136245
})

src/useFieldArray.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from 'react';
1+
import { useMemo } from 'react'
22
import { useForm, useField } from 'react-final-form'
33
import { fieldSubscriptionItems, ARRAY_ERROR } from 'final-form'
44
import { Mutators } from 'final-form-arrays'
@@ -13,6 +13,21 @@ const all: FieldSubscription = fieldSubscriptionItems.reduce((result, key) => {
1313
return result
1414
}, {} as FieldSubscription)
1515

16+
/**
17+
* handle synced errors
18+
*/
19+
const handleError = (error: string | readonly string[] | void) => {
20+
if (!error || Array.isArray(error)) {
21+
return error
22+
}
23+
const arrayError: string[] = []
24+
// gross, but we have to set a string key on the array
25+
;(arrayError as unknown as Record<string, string>)[ARRAY_ERROR] =
26+
error as string
27+
28+
return arrayError
29+
}
30+
1631
const useFieldArray = (
1732
name: string,
1833
{
@@ -26,33 +41,45 @@ const useFieldArray = (
2641
const form = useForm('useFieldArray')
2742

2843
const formMutators = form.mutators as unknown as Mutators
29-
const hasMutators = !!(formMutators && (formMutators as any).push && (formMutators as any).pop)
44+
const hasMutators = !!(
45+
formMutators &&
46+
(formMutators as any).push &&
47+
(formMutators as any).pop
48+
)
3049
if (!hasMutators) {
3150
throw new Error(
3251
'Array mutators not found. You need to provide the mutators from final-form-arrays to your form'
3352
)
3453
}
35-
const mutators = useMemo<Record<string, Function>>(() =>
36-
// curry the field name onto all mutator calls
37-
Object.keys(formMutators).reduce((result, key) => {
38-
result[key] = (...args: any[]) => (formMutators as any)[key](name, ...args)
39-
return result
40-
}, {} as Record<string, Function>
41-
), [name, formMutators])
54+
const mutators = useMemo<Record<string, Function>>(
55+
() =>
56+
// curry the field name onto all mutator calls
57+
Object.keys(formMutators).reduce(
58+
(result, key) => {
59+
result[key] = (...args: any[]) =>
60+
(formMutators as any)[key](name, ...args)
61+
return result
62+
},
63+
{} as Record<string, Function>
64+
),
65+
[name, formMutators]
66+
)
4267

4368
const validate: FieldValidator | undefined = useConstant(() =>
4469
!validateProp
4570
? undefined
4671
: (value: any, allValues: any, meta: any) => {
47-
const error = validateProp(value, allValues, meta)
48-
if (!error || Array.isArray(error)) {
49-
return error
50-
} else {
51-
const arrayError: any[] = []
52-
// gross, but we have to set a string key on the array
53-
; (arrayError as any)[ARRAY_ERROR] = error
54-
return arrayError
72+
const validation = validateProp(value, allValues, meta)
73+
if (!validation) {
74+
return undefined
75+
}
76+
77+
if (validation.then) {
78+
return validation.then((error: string | readonly string[] | void) =>
79+
handleError(error)
80+
)
5581
}
82+
return handleError(validation)
5683
}
5784
)
5885

@@ -62,7 +89,7 @@ const useFieldArray = (
6289
initialValue,
6390
isEqual,
6491
validate,
65-
format: v => v
92+
format: (v) => v
6693
})
6794

6895
// FIX #167: Don't destructure/spread meta object because it has lazy getters
@@ -82,7 +109,7 @@ const useFieldArray = (
82109
}
83110
}
84111

85-
const map = <T,>(iterator: (name: string, index: number) => T): T[] => {
112+
const map = <T>(iterator: (name: string, index: number) => T): T[] => {
86113
// required || for Flow, but results in uncovered line in Jest/Istanbul
87114
// istanbul ignore next
88115
const len = length || 0
@@ -110,4 +137,4 @@ const useFieldArray = (
110137
}
111138
}
112139

113-
export default useFieldArray
140+
export default useFieldArray

0 commit comments

Comments
 (0)