Skip to content

Commit 73ce137

Browse files
committed
feat: add option forceUpdateOnStatusChange to useForm()
1 parent fc533ba commit 73ce137

File tree

6 files changed

+58
-34
lines changed

6 files changed

+58
-34
lines changed

src/useForm.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@ export type UseFormOptions<V extends Values, E, R> = {
343343
* Disables the form (fields and buttons).
344344
*/
345345
disabled?: boolean;
346+
/**
347+
* Update the form when it is modified or touched (happens only at the form level).
348+
*/
349+
forceUpdateOnStatusChange?: boolean;
346350
/**
347351
* Sets the initial errors.
348352
*/
@@ -483,6 +487,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
483487
// todo v6: change to false by default
484488
disableSubmitIfNotModified = true,
485489
disableSubmitIfNotValid = false,
490+
forceUpdateOnStatusChange = false,
486491
initialErrors,
487492
initialModified,
488493
initialTouched,
@@ -555,6 +560,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
555560

556561
// Handle form status.
557562
const formStatus = useFormStatus({
563+
forceUpdateOnStatusChange,
558564
formState,
559565
mode
560566
})
@@ -564,6 +570,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
564570

565571
// Handle form values.
566572
const formValues = useFormValues<V, E, R>({
573+
forceUpdateOnStatusChange,
567574
formErrors,
568575
formKeys,
569576
formState,
@@ -701,7 +708,9 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
701708
const target = event.currentTarget ?? event.target
702709
const { name } = target
703710

704-
setTouchedField(name, true)
711+
setTouchedField(name, true, {
712+
forceUpdate: forceUpdateOnStatusChange
713+
})
705714

706715
if (validateOnTouch) {
707716
setNeedValidation([name])
@@ -716,7 +725,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
716725
setValue(name, value, { validate: validateOnTouch })
717726
}
718727
}
719-
}, [setTouchedField, validateOnTouch, trimOnBlur, setNeedValidation, getValue, setValue])
728+
}, [setTouchedField, forceUpdateOnStatusChange, validateOnTouch, trimOnBlur, setNeedValidation, getValue, setValue])
720729

721730
const handleChange = useCallback<UseFormHook<V, E, R>['handleChange']>((event, opts): void => {
722731
const { setValueOptions } = opts ?? {}
@@ -785,7 +794,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
785794

786795
if (props.disabled || formDisabled ||
787796
(state.hasError && disableSubmitIfNotValid && type === 'submit') ||
788-
(!state.modified && (
797+
(!state.modified && (mode === 'controlled' || forceUpdateOnStatusChange) && (
789798
// Disable submit button if form is not modified.
790799
(type === 'submit' && disableSubmitIfNotModified) ||
791800
// Disable reset button if form is not modified.
@@ -794,7 +803,7 @@ function useForm<V extends Values, E = Error, R = any> (options: UseFormOptions<
794803
result.disabled = true
795804
}
796805
return result
797-
}, [formDisabled, state.hasError, state.modified, disableSubmitIfNotValid, disableSubmitIfNotModified, handleButtonClick])
806+
}, [formDisabled, state.hasError, state.modified, disableSubmitIfNotValid, mode, forceUpdateOnStatusChange, disableSubmitIfNotModified])
798807

799808
/**
800809
* Returns props of a field.

src/useFormStatus.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
import { hasTrueValues } from './utils'
1616

1717
export type UseFormStatusOptions<V extends Values, E, R> = {
18+
/**
19+
* Update the form when it is modified or touched (happens only at the form level).
20+
*/
21+
forceUpdateOnStatusChange?: boolean;
1822
/**
1923
* The form state hook.
2024
*/
@@ -142,6 +146,7 @@ export type UseFormStatusHook<V extends Values> = {
142146

143147
function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V, E, R>): UseFormStatusHook<V> {
144148
const {
149+
forceUpdateOnStatusChange,
145150
formState,
146151
mode
147152
} = options
@@ -161,7 +166,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
161166
// MODIFIED
162167

163168
const clearModified = useCallback<UseFormStatusHook<V>['clearModified']>((paths, opts) => {
164-
const { forceUpdate = false } = opts ?? {}
169+
const { forceUpdate = forceUpdateOnStatusChange } = opts ?? {}
165170

166171
if (paths) {
167172
for (let i = 0; i < paths.length; i++) {
@@ -178,7 +183,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
178183
modifiedFields: modifiedRef.current
179184
}))
180185
}
181-
}, [mode, modifiedRef, setState])
186+
}, [forceUpdateOnStatusChange, mode, modifiedRef, setState])
182187

183188
const getModified = useCallback<UseFormStatusHook<V>['getModified']>(() => {
184189
return modifiedRef.current
@@ -192,7 +197,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
192197
}, [modifiedRef])
193198

194199
const resetModified = useCallback<UseFormStatusHook<V>['resetModified']>((paths, opts) => {
195-
const { forceUpdate = false } = opts ?? {}
200+
const { forceUpdate = forceUpdateOnStatusChange } = opts ?? {}
196201

197202
if (paths) {
198203
for (let i = 0; i < paths.length; i++) {
@@ -209,12 +214,12 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
209214
modifiedFields: modifiedRef.current
210215
}))
211216
}
212-
}, [initialModified, mode, modifiedRef, setState])
217+
}, [forceUpdateOnStatusChange, initialModified, mode, modifiedRef, setState])
213218

214219
const setModified = useCallback<UseFormStatusHook<V>['setModified']>((values, opts) => {
215220
const {
216221
partial = false,
217-
forceUpdate = false
222+
forceUpdate = forceUpdateOnStatusChange
218223
} = opts ?? {}
219224

220225
// const previousModified = clone(modifiedRef.current)
@@ -232,7 +237,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
232237
modifiedFields: nextModified
233238
}))
234239
}
235-
}, [mode, modifiedRef, setState])
240+
}, [forceUpdateOnStatusChange, mode, modifiedRef, setState])
236241

237242
const setModifiedField = useCallback<UseFormStatusHook<V>['setModifiedField']>((path, value) => {
238243
setModified({ [path]: value }, { partial: true })
@@ -241,7 +246,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
241246
// TOUCHED
242247

243248
const clearTouched = useCallback<UseFormStatusHook<V>['clearTouched']>((paths, opts) => {
244-
const { forceUpdate = false } = opts ?? {}
249+
const { forceUpdate = forceUpdateOnStatusChange } = opts ?? {}
245250

246251
if (paths) {
247252
for (let i = 0; i < paths.length; i++) {
@@ -258,7 +263,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
258263
touchedFields: touchedRef.current
259264
}))
260265
}
261-
}, [mode, setState, touchedRef])
266+
}, [forceUpdateOnStatusChange, mode, setState, touchedRef])
262267

263268
const getTouched = useCallback<UseFormStatusHook<V>['getTouched']>(() => {
264269
return touchedRef.current
@@ -272,7 +277,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
272277
}, [touchedRef])
273278

274279
const resetTouched = useCallback<UseFormStatusHook<V>['resetTouched']>((paths, opts) => {
275-
const { forceUpdate = false } = opts ?? {}
280+
const { forceUpdate = forceUpdateOnStatusChange } = opts ?? {}
276281

277282
if (paths) {
278283
for (let i = 0; i < paths.length; i++) {
@@ -289,12 +294,12 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
289294
touchedFields: touchedRef.current
290295
}))
291296
}
292-
}, [initialTouched, mode, setState, touchedRef])
297+
}, [forceUpdateOnStatusChange, initialTouched, mode, setState, touchedRef])
293298

294299
const setTouched = useCallback<UseFormStatusHook<V>['setTouched']>((values, opts) => {
295300
const {
296-
partial = false,
297-
forceUpdate = false
301+
forceUpdate = forceUpdateOnStatusChange,
302+
partial = false
298303
} = opts ?? {}
299304

300305
// const previousTouched = clone(touchedRef.current)
@@ -312,7 +317,7 @@ function useFormStatus<V extends Values, E, R> (options: UseFormStatusOptions<V,
312317
touchedFields: nextTouched
313318
}))
314319
}
315-
}, [mode, setState, touchedRef])
320+
}, [forceUpdateOnStatusChange, mode, setState, touchedRef])
316321

317322
const setTouchedField = useCallback<UseFormStatusHook<V>['setTouchedField']>((path, value) => {
318323
setTouched({ [path]: value }, { partial: true })

src/useFormValues.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import { UseFormErrorsHook } from './useFormErrors'
2323
import deepExtend from '@jalik/deep-extend'
2424

2525
export type UseFormValuesOptions<V extends Values, E, R> = {
26+
/**
27+
* Update the form when it is modified or touched (happens only at the form level).
28+
*/
29+
forceUpdateOnStatusChange?: boolean;
2630
/**
2731
* The form keys hook.
2832
*/
@@ -161,6 +165,7 @@ export type UseFormValuesHook<V extends Values> = {
161165

162166
function useFormValues<V extends Values, E, R> (options: UseFormValuesOptions<V, E, R>): UseFormValuesHook<V> {
163167
const {
168+
forceUpdateOnStatusChange,
164169
formErrors,
165170
formKeys,
166171
formState,
@@ -286,10 +291,22 @@ function useFormValues<V extends Values, E, R> (options: UseFormValuesOptions<V,
286291
// Update values ref.
287292
valuesRef.current = nextValues
288293

294+
if (initialize) {
295+
initialValuesRef.current = clone(nextValues)
296+
initializedRef.current = true
297+
}
298+
if (updateErrors) {
299+
errorsRef.current = partial ? { ...errorsRef.current, ...nextErrors } : nextErrors
300+
}
301+
if (updateModified) {
302+
modifiedRef.current = partial ? { ...modifiedRef.current, ...nextModified } : nextModified
303+
}
304+
if (updateTouched) {
305+
touchedRef.current = partial ? { ...touchedRef.current } : {}
306+
}
307+
289308
// Update state.
290-
if (mode === 'controlled' || forceUpdate || (initialize || !initializedRef.current) || validate ||
291-
hasDefinedValues(nextErrors) ||
292-
hasDefinedValues(nextModified)) {
309+
if (mode === 'controlled' || forceUpdate || validate || (initialize || !initializedRef.current) || (forceUpdateOnStatusChange && hasDefinedValues(nextModified))) {
293310
setState((s) => {
294311
const nextState = {
295312
...s,
@@ -300,23 +317,18 @@ function useFormValues<V extends Values, E, R> (options: UseFormValuesOptions<V,
300317

301318
// Update initial values.
302319
if (initialize) {
303-
initialValuesRef.current = clone(nextValues)
304320
nextState.initialValues = initialValuesRef.current
305-
initializedRef.current = true
306321
}
307322
// Update errors.
308323
if (updateErrors) {
309-
errorsRef.current = partial ? { ...errorsRef.current, ...nextErrors } : nextErrors
310324
nextState.errors = errorsRef.current
311325
}
312326
// Update modified.
313327
if (updateModified) {
314-
modifiedRef.current = partial ? { ...modifiedRef.current, ...nextModified } : nextModified
315328
nextState.modifiedFields = modifiedRef.current
316329
}
317330
// Update touched.
318331
if (updateTouched) {
319-
touchedRef.current = partial ? { ...touchedRef.current } : {}
320332
nextState.touchedFields = touchedRef.current
321333
}
322334
if (validate) {
@@ -353,7 +365,7 @@ function useFormValues<V extends Values, E, R> (options: UseFormValuesOptions<V,
353365
watchers.current.emit(inputChangeEvent(path), status)
354366
}
355367
}
356-
}, [errorsRef, getInitialValue, initialValuesRef, initializedRef, isTouched, mode, modifiedRef, options.nullify, replaceKeys, setState, touchedRef, valuesRef, watchers])
368+
}, [errorsRef, forceUpdateOnStatusChange, getInitialValue, initialValuesRef, initializedRef, isTouched, mode, modifiedRef, options.nullify, replaceKeys, setState, touchedRef, valuesRef, watchers])
357369

358370
const setValue = useCallback<UseFormValuesHook<V>['setValue']>((path, value, opts) => {
359371
const currentValue = getValue(path)
@@ -418,11 +430,8 @@ function useFormValues<V extends Values, E, R> (options: UseFormValuesOptions<V,
418430
}
419431
}
420432
initialValuesRef.current = nextInitialValues
421-
clearErrors(paths, { forceUpdate: false })
422-
clearModified(paths, { forceUpdate: false })
423-
clearTouched(paths, { forceUpdate: false })
424433
clearValues(paths, { ...opts })
425-
}, [clearErrors, clearModified, clearTouched, clearValues, initialValuesRef])
434+
}, [clearValues, initialValuesRef])
426435

427436
const resetValues = useCallback<UseFormValuesHook<V>['resetValues']>((paths, opts) => {
428437
let nextValues: PathsOrValues<V>

test/useForm.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,8 @@ describe('useForm()', () => {
556556
result.current.reset(['password'])
557557
})
558558

559-
expect(result.current.modifiedFields.username).toBe(true)
560-
expect(result.current.modifiedFields.password).toBeFalsy()
559+
expect(result.current.isModified('username')).toBe(true)
560+
expect(result.current.isModified('password')).toBeFalsy()
561561
expect(result.current.values.username).toBe('test')
562562
expect(result.current.values.password).toBe(initialValues.password)
563563
})

test/useForm/clearErrors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function test (mode: FormMode) {
2121
expect(hook.result.current.getError('a')).toBe('invalid')
2222
expect(hook.result.current.getError('b')).toBe('invalid')
2323
act(() => hook.result.current.clearErrors())
24-
expect(hook.result.current.errors).toStrictEqual({})
24+
expect(hook.result.current.getErrors()).toStrictEqual({})
2525
})
2626
})
2727

test/useForm/options/disableSubmitIfNotModified.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ function tests (mode: FormMode) {
1717
const hook = renderHook(() => useForm({
1818
mode,
1919
initialValues,
20-
disableSubmitIfNotModified: true
20+
disableSubmitIfNotModified: true,
21+
forceUpdateOnStatusChange: true
2122
}))
2223
expect(hook.result.current.isModified()).toBe(false)
2324
expect(hook.result.current.getButtonProps({ type: 'submit' }).disabled).toBe(true)

0 commit comments

Comments
 (0)