@@ -74,26 +74,20 @@ function PydanticFormContextProvider({
74
74
new Map < string , object > ( ) ,
75
75
) ;
76
76
const [ formInputData , setFormInputData ] = useState < object [ ] > ( [ ] ) ;
77
-
78
77
const formRef = useRef < string > ( formKey ) ;
79
78
80
79
const updateHistory = useCallback (
81
- async ( formInput : object , previousSteps : object [ ] ) => {
82
- const hashOfPreviousSteps = await getHashForArray ( previousSteps ) ;
83
- setFormInputHistory ( ( prevState ) =>
84
- prevState . set ( hashOfPreviousSteps , formInput ) ,
80
+ async ( previousStepsData : object [ ] , formInputData : object ) => {
81
+ const hashOfPreviousSteps = await getHashForArray (
82
+ previousStepsData ,
83
+ ) ;
84
+ setFormInputHistory ( ( currentState ) =>
85
+ currentState . set ( hashOfPreviousSteps , formInputData ) ,
85
86
) ;
86
87
} ,
87
88
[ ] ,
88
89
) ;
89
90
90
- const goToPreviousStep = ( formInput : object ) => {
91
- setFormInputData ( ( prevState ) => {
92
- updateHistory ( formInput , prevState ) ;
93
- return prevState . slice ( 0 , - 1 ) ;
94
- } ) ;
95
- } ;
96
-
97
91
const [ errorDetails , setErrorDetails ] =
98
92
useState < PydanticFormValidationErrorDetails > ( ) ;
99
93
@@ -166,58 +160,49 @@ function PydanticFormContextProvider({
166
160
167
161
/*
168
162
This method resets the form and makes sure it waits for the reset to complete
169
- before proceeding. If it finds data in form history, it uses that data to reset the form.
163
+ before proceeding. If it finds data in formHistory based on the hash of the previo
164
+ us steps, it uses that data to prefill the form.
170
165
*/
171
166
const awaitReset = useCallback (
172
- async ( payLoad ?: FieldValues ) => {
173
- await getHashForArray ( formInputData ) . then ( ( hash ) => {
174
- let resetPayload = { } ;
175
-
176
- if ( payLoad ) {
177
- resetPayload = { ...payLoad } ;
178
- } else {
179
- const currentStepFromHistory = formInputHistory . get ( hash ) ;
180
-
181
- if ( currentStepFromHistory ) {
182
- resetPayload = { ...currentStepFromHistory } ;
183
- }
184
- }
185
- reactHookForm . reset ( resetPayload ) ;
186
- } ) ;
167
+ async ( payLoad : FieldValues = { } ) => {
168
+ try {
169
+ reactHookForm . reset ( payLoad ) ;
170
+
171
+ // This is a workaround to we wait for the reset to complete
172
+ // https://gemini.google.com/app/26d8662d603d6322?hl=nl
173
+ return new Promise < void > ( ( resolve ) => {
174
+ setTimeout ( ( ) => {
175
+ resolve ( ) ;
176
+ } , 0 ) ;
177
+ } ) ;
178
+ } catch ( error ) {
179
+ console . error ( 'Failed to reactHookFOrm' , error ) ;
180
+ }
187
181
} ,
188
-
189
- [ formInputData , formInputHistory , reactHookForm ] ,
182
+ [ reactHookForm ] ,
190
183
) ;
191
184
192
- const addFormInputData = useCallback (
193
- ( formInput : object , replaceInsteadOfAdd = false ) => {
194
- setFormInputData ( ( currentInputs ) => {
195
- const data = replaceInsteadOfAdd
196
- ? currentInputs . slice ( 0 , - 1 )
197
- : currentInputs ;
198
- updateHistory ( formInput , data ) ;
199
- return [ ...data , { ...formInput } ] ;
200
- } ) ;
185
+ const addFormInputData = useCallback ( ( ) => {
186
+ setFormInputData ( ( currentFormInputData ) => {
187
+ // Note. If we don't use cloneDeep here we are adding a reference to the reactHookFormValues
188
+ // that changes on every change in the form and triggering effects before we want to.
189
+ const reactHookFormValues = _ . cloneDeep ( reactHookForm . getValues ( ) ) ;
190
+ updateHistory ( currentFormInputData , reactHookFormValues ) ;
191
+ // We call reset right after using the values to make sure
192
+ // values in reactHookForm are cleared. This avoids some
193
+ // race condition errors where reactHookForm data was still set where
194
+ // we did not expect it to be.
201
195
awaitReset ( ) ;
202
- } ,
203
- [ awaitReset , setFormInputData , updateHistory ] ,
204
- ) ;
196
+ return [ ... currentFormInputData , { ... reactHookFormValues } ] ;
197
+ } ) ;
198
+ } , [ ] ) ;
205
199
206
200
const submitFormFn = useCallback ( ( ) => {
207
201
setIsSending ( true ) ;
208
- const reactHookFormValues = _ . cloneDeep ( reactHookForm . getValues ( ) ) ;
209
- awaitReset ( ) ;
210
- // Note. If we don't use cloneDeep here we are adding a reference to the reactHookFormValues
211
- // that changes on every change in the form and triggering effects before we want to.
212
- addFormInputData ( reactHookFormValues , ! ! errorDetails ) ;
202
+ setErrorDetails ( undefined ) ;
203
+ addFormInputData ( ) ;
213
204
window . scrollTo ( 0 , 0 ) ;
214
- } , [
215
- reactHookForm ,
216
- errorDetails ,
217
- addFormInputData ,
218
- awaitReset ,
219
- setIsSending ,
220
- ] ) ;
205
+ } , [ ] ) ;
221
206
222
207
const onClientSideError = useCallback (
223
208
( data ?: FieldValues ) => {
@@ -230,6 +215,16 @@ function PydanticFormContextProvider({
230
215
[ reactHookForm , submitFormFn ] ,
231
216
) ;
232
217
218
+ const goToPreviousStep = ( ) => {
219
+ setFormInputData ( ( currentFormInputData ) => {
220
+ // Stores any data that is entered but not submitted yet to be
221
+ // able to restore later
222
+ const reactHookFormValues = _ . cloneDeep ( reactHookForm . getValues ( ) ) ;
223
+ updateHistory ( currentFormInputData , reactHookFormValues ) ;
224
+ return currentFormInputData . slice ( 0 , - 1 ) ;
225
+ } ) ;
226
+ } ;
227
+
233
228
const submitForm = reactHookForm . handleSubmit (
234
229
submitFormFn ,
235
230
onClientSideError ,
@@ -245,23 +240,12 @@ function PydanticFormContextProvider({
245
240
[ awaitReset , reactHookForm ] ,
246
241
) ;
247
242
248
- const resetErrorDetails = useCallback ( ( ) => {
249
- setErrorDetails ( undefined ) ;
250
- } , [ ] ) ;
251
-
252
243
const isLoading =
253
244
isLoadingFormLabels ||
254
245
isLoadingSchema ||
255
246
isParsingSchema ||
256
247
( customDataProvider ? isLoadingCustomData : false ) ;
257
248
258
- const clearForm = useCallback ( ( ) => {
259
- setFormInputData ( [ ] ) ;
260
- setIsFullFilled ( false ) ;
261
- setRawSchema ( emptyRawSchema ) ;
262
- setHasNext ( false ) ;
263
- } , [ emptyRawSchema ] ) ;
264
-
265
249
const fieldDataStorageRef = useRef < Map < string , Map < string , unknown > > > (
266
250
new Map ( ) ,
267
251
) ;
@@ -299,9 +283,6 @@ function PydanticFormContextProvider({
299
283
) ;
300
284
301
285
const PydanticFormContextState = {
302
- // to prevent an issue where the sending state hangs
303
- // we check both the SWR hook state as our manual state
304
- clearForm,
305
286
config,
306
287
customDataProvider,
307
288
errorDetails,
@@ -314,74 +295,90 @@ function PydanticFormContextProvider({
314
295
isLoading,
315
296
isSending : isSending && isLoadingSchema ,
316
297
onCancel,
317
- onPrevious : ( ) => goToPreviousStep ( reactHookForm ?. getValues ( ) ) ,
298
+ onPrevious : ( ) => goToPreviousStep ( ) ,
318
299
pydanticFormSchema,
319
300
reactHookForm,
320
- resetErrorDetails,
321
301
resetForm,
322
302
submitForm,
323
303
title,
324
304
} ;
325
305
326
306
// useEffect to handle API responses
327
307
useEffect ( ( ) => {
308
+ const restoreHistory = async ( ) => {
309
+ await getHashForArray ( formInputData )
310
+ . then ( ( hash ) => {
311
+ if ( formInputHistory . has ( hash ) ) {
312
+ awaitReset ( formInputHistory . get ( hash ) as FieldValues ) ;
313
+ } else {
314
+ awaitReset ( ) ;
315
+ }
316
+ } )
317
+ . catch ( ( ) => {
318
+ console . error ( 'Failed to hash form input data' ) ;
319
+ } ) ;
320
+ } ;
321
+
328
322
if ( ! apiResponse ) {
329
323
return ;
330
324
}
331
- // when we receive errors, we append to the scheme
325
+
332
326
if ( apiResponse ?. validation_errors ) {
333
- // Restore the data we got the error with
334
- const errorPayload = [ ...formInputData ] . pop ( ) ;
335
- awaitReset ( errorPayload ) ;
327
+ // Restore the data we got the error with and remove it from
328
+ // formInputData so we can add it again
329
+ setFormInputData ( ( currentData ) => {
330
+ const nextData = [ ...currentData ] ;
331
+ const errorPayload = nextData . pop ( ) ;
332
+ awaitReset ( errorPayload ) ;
333
+ return nextData ;
334
+ } ) ;
335
+
336
336
setErrorDetails ( getErrorDetailsFromResponse ( apiResponse ) ) ;
337
337
return ;
338
338
}
339
339
340
- awaitReset ( ) ;
341
340
if ( apiResponse ?. success ) {
342
341
setIsFullFilled ( true ) ;
343
342
return ;
344
343
}
345
344
346
- // when we receive a new form from JSON, we fully reset the form
347
345
if ( apiResponse ?. form && rawSchema !== apiResponse . form ) {
348
346
setRawSchema ( apiResponse . form ) ;
349
347
if ( apiResponse . meta ) {
350
348
setHasNext ( ! ! apiResponse . meta . hasNext ) ;
351
349
}
352
- setErrorDetails ( undefined ) ;
350
+ restoreHistory ( ) ;
353
351
}
354
352
355
353
setIsSending ( false ) ;
356
354
// eslint-disable-next-line react-hooks/exhaustive-deps
357
355
} , [ apiResponse ] ) ; // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
358
356
359
- // Useeffect to the form input data if the formKey changes
357
+ // useEffect to the form input data if the formKey changes
360
358
useEffect ( ( ) => {
361
359
if ( formKey !== formRef . current ) {
362
360
setFormInputData ( [ ] ) ;
363
361
setFormInputHistory ( new Map < string , object > ( ) ) ;
364
- awaitReset ( { } ) ;
365
362
formRef . current = formKey ;
366
363
}
367
- } , [ awaitReset , formKey ] ) ;
364
+ } , [ formKey ] ) ;
368
365
369
- // UseEffect to handle successfull submits
366
+ // useEffect to handle successfull submits
370
367
useEffect ( ( ) => {
371
368
if ( ! isFullFilled ) {
372
369
return ;
373
370
}
374
371
375
372
if ( onSuccess ) {
376
- const values = reactHookForm . getValues ( ) ;
377
- onSuccess ( values , apiResponse || { } ) ;
373
+ const reactHookFormValues = _ . cloneDeep ( reactHookForm . getValues ( ) ) ;
374
+ onSuccess ( reactHookFormValues , apiResponse || { } ) ;
378
375
}
379
376
380
377
setFormInputHistory ( new Map < string , object > ( ) ) ;
381
378
// eslint-disable-next-line react-hooks/exhaustive-deps
382
- } , [ apiResponse , isFullFilled ] ) ; // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
379
+ } , [ isFullFilled ] ) ; // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
383
380
384
- // UseEffect to handles errors throws by the useApiProvider call
381
+ // useEffect to handles errors throws by the useApiProvider call
385
382
// for instance unexpected 500 errors
386
383
useEffect ( ( ) => {
387
384
if ( ! error ) {
@@ -395,7 +392,7 @@ function PydanticFormContextProvider({
395
392
} ) ;
396
393
} , [ error ] ) ;
397
394
398
- // UseEffect to handle locale change
395
+ // useEffect to handle locale change
399
396
useEffect ( ( ) => {
400
397
const getLocale = ( ) => {
401
398
switch ( locale ) {
0 commit comments