Skip to content
This repository was archived by the owner on Apr 9, 2025. It is now read-only.

Commit 33d7d3f

Browse files
authored
Merge pull request #75 from dbssman/feature/54-loading-states-validating-submitting
Feature/54 loading states validating submitting
2 parents 589d2de + e818cea commit 33d7d3f

File tree

8 files changed

+108
-11
lines changed

8 files changed

+108
-11
lines changed

docs/api/use-form-handler/form-state.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ Provides you with the reactive state of the form, including validation, dirty an
44

55
## Return
66

7-
| attribute | type | description |
8-
| --------- | ------------------------- | -------------------------------------------------------------- |
9-
| dirty | `Record<string, boolean>` | Object containing all the inputs that have been modified |
10-
| errors | `Record<string, string>` | Object containing all the current field errors of the form |
11-
| touched | `Record<string, boolean>` | Object containing all the inputs the users has interacted with |
12-
| isDirty | `boolean` | True if there is any modified field on the form |
13-
| isTouched | `boolean` | True if there has been any interaction with a form field |
14-
| isValid | `boolean` | True if there are no form errors |
7+
| attribute | type | description |
8+
| ------------ | ------------------------- | -------------------------------------------------------------- |
9+
| dirty | `Record<string, boolean>` | Object containing all the fields that have been modified |
10+
| errors | `Record<string, string>` | Object containing all the current field errors of the form |
11+
| touched | `Record<string, boolean>` | Object containing all the fields the users has interacted with |
12+
| validating | `Record<string, boolean>` | Object containing all the fields undergoing validation |
13+
| isDirty | `boolean` | True if there is any modified field on the form |
14+
| isTouched | `boolean` | True if there has been any interaction with a form field |
15+
| isValid | `boolean` | True if there are no form errors |
16+
| isValidating | `boolean` | True if there are field validations in progress |
1517

1618
## Rules
1719

@@ -58,8 +60,10 @@ export interface FormState<T> {
5860
isDirty: boolean
5961
isTouched: boolean
6062
isValid: boolean
63+
isValidating: boolean
6164
dirty: Record<keyof T, boolean>
6265
touched: Record<keyof T, boolean>
6366
errors: Record<keyof T, string | undefined>
67+
validating: Record<keyof T, boolean>
6468
}
6569
```

docs/api/use-form-handler/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export declare const useFormHandler: <
182182
readonly isDirty: boolean
183183
readonly isTouched: boolean
184184
readonly isValid: boolean
185+
readonly isValidating: boolean
185186
readonly dirty: import('@vue/reactivity').DeepReadonly<
186187
import('@vue/reactivity').UnwrapRef<Record<keyof T, boolean>>
187188
>
@@ -191,6 +192,9 @@ export declare const useFormHandler: <
191192
readonly errors: import('@vue/reactivity').DeepReadonly<
192193
import('@vue/reactivity').UnwrapRef<Record<keyof T, string | undefined>>
193194
>
195+
readonly validating: import('@vue/reactivity').DeepReadonly<
196+
import('@vue/reactivity').UnwrapRef<Record<keyof T, boolean>>
197+
>
194198
}
195199
handleSubmit: (
196200
successFn: HandleSubmitSuccessFn,

docs/api/use-form-handler/register.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Coming soon...
3939
| disabled | `boolean` | Disabled state binding for the field |
4040
| isDirty | `boolean` | Dirty state binding for the field. Only returned if `withDetails` is true |
4141
| isTouched | `boolean` | Touched state binding for the field. Only returned if `withDetails` is true |
42+
| isValidating | `boolean` | Validating state binding for the field. Only returned if `withDetails` is true |
4243
| onChange | `(el: any) => Promise<void>` | Value update handler for native inputs |
4344
| required | `boolean \| string` | Native required validation. Only returned if `useNativeValidations` is set to true and `required` is set. |
4445
| min | `number \| Object` | Native min validation. Only returned if `useNativeValidations` is set to true and `min` is set. |
@@ -243,7 +244,6 @@ Custom validations are kept very simple, can be synchronous or asynchronous. We
243244
## Type Declarations
244245

245246
```ts
246-
247247
interface ValidationWithMessage {
248248
value: number | string | RegExp
249249
message: string
@@ -281,6 +281,7 @@ export type Register = (
281281
onChange?: (() => Promise<void>) | undefined
282282
isDirty?: boolean | undefined
283283
isTouched?: boolean | undefined
284+
isValidating?: boolean | undefined
284285
disabled?: boolean | undefined
285286
name: keyof T
286287
modelValue: T[keyof T]

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const BaseInputProps = {
66
name: { type: String, required: true },
77
isDirty: { type: Boolean, default: () => false },
88
isTouched: { type: Boolean, default: () => false },
9+
isValidating: { type: Boolean, default: () => false },
910
disabled: { type: Boolean, default: () => false },
1011
error: { type: String, default: () => '' },
1112
onBlur: { type: Function, required: true },

src/types/baseControl.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface BaseControlProps<
2929
/** Current touched state of the control */
3030
isTouched?: boolean
3131

32+
/** Current validating state of the control */
33+
isValidating?: boolean
34+
3235
/** Handler binding for native inputs */
3336
onChange?: (el: any) => Promise<void>
3437
}

src/types/formHandler.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@ export interface FormState<T> {
1111
isDirty: boolean
1212
isTouched: boolean
1313
isValid: boolean
14+
isValidating: boolean
1415
dirty: Record<keyof T, boolean>
1516
touched: Record<keyof T, boolean>
1617
errors: Record<keyof T, string | undefined>
18+
validating: Record<keyof T, boolean>
1719
}
1820

1921
/** Optional function to be called after a form failed to submit */
2022
export type HandleSubmitErrorFn<T> = (errors: FormState<T>['errors']) => void
2123

2224
/** Expected function to be called after a form submitted successfully */
23-
export type HandleSubmitSuccessFn<T> = (values: Record<keyof T, any>) => void
25+
export type HandleSubmitSuccessFn<T> = (
26+
values: Record<keyof T, any>
27+
) => PossiblePromise<void>
2428

2529
export type Build<T extends Record<string, any> = Record<string, any>> = <
2630
TBuild extends Partial<T>,

src/useFormHandler.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ export const initialState = () => ({
3434
touched: {},
3535
dirty: {},
3636
errors: {},
37+
validating: {},
3738
isDirty: false,
3839
isTouched: false,
3940
isValid: true,
41+
isValidating: false,
4042
})
4143

4244
type Refs<T> = Record<keyof T, WrappedReference>
@@ -106,6 +108,10 @@ export const useFormHandler = <
106108
formState.isTouched = Object.values(formState.touched).some(Boolean)
107109
}
108110

111+
const _updateValidatingState = () => {
112+
formState.isValidating = Object.values(formState.validating).some(Boolean)
113+
}
114+
109115
const _validateField = async <T>({
110116
name,
111117
values,
@@ -118,14 +124,21 @@ export const useFormHandler = <
118124
if (_refs[name]._disabled) {
119125
return
120126
}
127+
const timeout = setTimeout(
128+
() => setValidating(name as keyof TForm, true),
129+
20
130+
)
121131
for (const validation of Object.values(_refs[name]._validations)) {
122132
const result = await (validation as any)(values[name])
133+
123134
if (result !== true) {
124135
formState.errors[name] = result
125136
break
126137
}
127138
delete formState.errors[name]
128139
}
140+
clearTimeout(timeout)
141+
setValidating(name as keyof TForm, false)
129142
}
130143

131144
const _validateForm = async <T extends Record<string, any>>(
@@ -184,6 +197,19 @@ export const useFormHandler = <
184197
}
185198
}
186199

200+
const setValidating = (name: keyof TForm, validating: boolean) => {
201+
console.log(validating)
202+
if (formState.validating[name] !== validating) {
203+
if (validating) {
204+
formState.validating[name] = true
205+
_updateValidatingState()
206+
return
207+
}
208+
delete formState.validating[name]
209+
_updateValidatingState()
210+
}
211+
}
212+
187213
const resetField = (name: keyof TForm) => {
188214
values[name] = _getInitial(name)
189215
setTouched(name, false)
@@ -319,6 +345,7 @@ export const useFormHandler = <
319345
...(withDetails && {
320346
isDirty: !!formState.dirty[name],
321347
isTouched: !!formState.touched[name],
348+
isValidating: !!formState.validating[name],
322349
}),
323350
...(native !== false && {
324351
onChange: async () => {

test/useFormHandler.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { initialState, useFormHandler } from '@/useFormHandler'
33
import { expect, it, describe } from 'vitest'
44
import { retry } from './testing-utils'
55

6+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7+
68
describe('useFormHandler()', () => {
79
describe('register()', () => {
810
it('should register a field', () => {
@@ -12,6 +14,7 @@ describe('useFormHandler()', () => {
1214
expect(field.onBlur).toBeDefined()
1315
expect(field.isDirty).toBeUndefined()
1416
expect(field.isTouched).toBeUndefined()
17+
expect(field.isValidating).toBeUndefined()
1518
expect(field.onClear).toBeDefined()
1619
expect(field.onChange).toBeDefined()
1720
expect(field.modelValue).toBe(null)
@@ -37,11 +40,12 @@ describe('useFormHandler()', () => {
3740
expect(field.modelValue).toBeDefined()
3841
expect(field['onUpdate:modelValue']).toBeDefined()
3942
})
40-
it('should apply dirty and touched states when withDetails is specified', () => {
43+
it('should apply dirty, touched and validation states when withDetails is specified', () => {
4144
const { register } = useFormHandler()
4245
const field = register('field', { withDetails: true })
4346
expect(field.isDirty).toBeDefined()
4447
expect(field.isTouched).toBeDefined()
48+
expect(field.isValidating).toBeDefined()
4549
})
4650
it('should apply default value', () => {
4751
const { values, register } = useFormHandler()
@@ -142,6 +146,55 @@ describe('useFormHandler()', () => {
142146
expect(values.field).toBe(null)
143147
expect(formState.isDirty).toBeTruthy()
144148
})
149+
it('should set validating state when async validation is in progress', async () => {
150+
const { register, formState, triggerValidation } = useFormHandler()
151+
register('field', {
152+
validate: {
153+
asyncValidation: async (value: string) => {
154+
await sleep(200)
155+
return value === 'test' || 'error'
156+
},
157+
},
158+
})
159+
triggerValidation('field')
160+
await sleep(20)
161+
expect(formState.isValidating).toBeTruthy()
162+
expect(formState.validating).toStrictEqual({ field: true })
163+
await sleep(200)
164+
expect(formState.isValidating).toBeFalsy()
165+
expect(formState.validating).toStrictEqual({})
166+
})
167+
it('should correctly validate when async validation fails', async () => {
168+
const { register, formState, triggerValidation } = useFormHandler()
169+
register('field', {
170+
validate: {
171+
asyncValidation: async (value: string) => {
172+
await sleep(200)
173+
return value === 'test' || 'error'
174+
},
175+
},
176+
})
177+
triggerValidation('field')
178+
await sleep(200)
179+
expect(formState.errors.field).toBe('error')
180+
expect(formState.isValid).toBeFalsy()
181+
expect(formState.validating).toStrictEqual({})
182+
expect(formState.isValidating).toBeFalsy()
183+
})
184+
it('should not set validating state for sync validation', async () => {
185+
const { register, formState, triggerValidation } = useFormHandler()
186+
register('field', {
187+
validate: {
188+
error: (value: string) => value === 'test' || 'error',
189+
},
190+
})
191+
triggerValidation('field')
192+
await sleep(10)
193+
expect(formState.errors.field).toBe('error')
194+
expect(formState.isValid).toBeFalsy()
195+
expect(formState.isValidating).toBeFalsy()
196+
expect(formState.validating).toStrictEqual({})
197+
})
145198
it('should set an error programmatically', async () => {
146199
const { formState, setError } = useFormHandler()
147200
setError('field', 'some error')

0 commit comments

Comments
 (0)