Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/54 loading states validating submitting #75

Merged
merged 8 commits into from
Jun 22, 2024
20 changes: 12 additions & 8 deletions docs/api/use-form-handler/form-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ Provides you with the reactive state of the form, including validation, dirty an

## Return

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

## Rules

Expand Down Expand Up @@ -58,8 +60,10 @@ export interface FormState<T> {
isDirty: boolean
isTouched: boolean
isValid: boolean
isValidating: boolean
dirty: Record<keyof T, boolean>
touched: Record<keyof T, boolean>
errors: Record<keyof T, string | undefined>
validating: Record<keyof T, boolean>
}
```
4 changes: 4 additions & 0 deletions docs/api/use-form-handler/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export declare const useFormHandler: <
readonly isDirty: boolean
readonly isTouched: boolean
readonly isValid: boolean
readonly isValidating: boolean
readonly dirty: import('@vue/reactivity').DeepReadonly<
import('@vue/reactivity').UnwrapRef<Record<keyof T, boolean>>
>
Expand All @@ -191,6 +192,9 @@ export declare const useFormHandler: <
readonly errors: import('@vue/reactivity').DeepReadonly<
import('@vue/reactivity').UnwrapRef<Record<keyof T, string | undefined>>
>
readonly validating: import('@vue/reactivity').DeepReadonly<
import('@vue/reactivity').UnwrapRef<Record<keyof T, boolean>>
>
}
handleSubmit: (
successFn: HandleSubmitSuccessFn,
Expand Down
3 changes: 2 additions & 1 deletion docs/api/use-form-handler/register.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Coming soon...
| disabled | `boolean` | Disabled state binding for the field |
| isDirty | `boolean` | Dirty state binding for the field. Only returned if `withDetails` is true |
| isTouched | `boolean` | Touched state binding for the field. Only returned if `withDetails` is true |
| isValidating | `boolean` | Validating state binding for the field. Only returned if `withDetails` is true |
| onChange | `(el: any) => Promise<void>` | Value update handler for native inputs |
| required | `boolean \| string` | Native required validation. Only returned if `useNativeValidations` is set to true and `required` is set. |
| min | `number \| Object` | Native min validation. Only returned if `useNativeValidations` is set to true and `min` is set. |
Expand Down Expand Up @@ -243,7 +244,6 @@ Custom validations are kept very simple, can be synchronous or asynchronous. We
## Type Declarations

```ts

interface ValidationWithMessage {
value: number | string | RegExp
message: string
Expand Down Expand Up @@ -281,6 +281,7 @@ export type Register = (
onChange?: (() => Promise<void>) | undefined
isDirty?: boolean | undefined
isTouched?: boolean | undefined
isValidating?: boolean | undefined
disabled?: boolean | undefined
name: keyof T
modelValue: T[keyof T]
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const BaseInputProps = {
name: { type: String, required: true },
isDirty: { type: Boolean, default: () => false },
isTouched: { type: Boolean, default: () => false },
isValidating: { type: Boolean, default: () => false },
disabled: { type: Boolean, default: () => false },
error: { type: String, default: () => '' },
onBlur: { type: Function, required: true },
Expand Down
3 changes: 3 additions & 0 deletions src/types/baseControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export interface BaseControlProps<
/** Current touched state of the control */
isTouched?: boolean

/** Current validating state of the control */
isValidating?: boolean

/** Handler binding for native inputs */
onChange?: (el: any) => Promise<void>
}
Expand Down
6 changes: 5 additions & 1 deletion src/types/formHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ export interface FormState<T> {
isDirty: boolean
isTouched: boolean
isValid: boolean
isValidating: boolean
dirty: Record<keyof T, boolean>
touched: Record<keyof T, boolean>
errors: Record<keyof T, string | undefined>
validating: Record<keyof T, boolean>
}

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

/** Expected function to be called after a form submitted successfully */
export type HandleSubmitSuccessFn<T> = (values: Record<keyof T, any>) => void
export type HandleSubmitSuccessFn<T> = (
values: Record<keyof T, any>
) => PossiblePromise<void>

export type Build<T extends Record<string, any> = Record<string, any>> = <
TBuild extends Partial<T>,
Expand Down
27 changes: 27 additions & 0 deletions src/useFormHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ export const initialState = () => ({
touched: {},
dirty: {},
errors: {},
validating: {},
isDirty: false,
isTouched: false,
isValid: true,
isValidating: false,
})

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

const _updateValidatingState = () => {
formState.isValidating = Object.values(formState.validating).some(Boolean)
}

const _validateField = async <T>({
name,
values,
Expand All @@ -118,14 +124,21 @@ export const useFormHandler = <
if (_refs[name]._disabled) {
return
}
const timeout = setTimeout(
() => setValidating(name as keyof TForm, true),
20
)
for (const validation of Object.values(_refs[name]._validations)) {
const result = await (validation as any)(values[name])

if (result !== true) {
formState.errors[name] = result
break
}
delete formState.errors[name]
}
clearTimeout(timeout)
setValidating(name as keyof TForm, false)
}

const _validateForm = async <T extends Record<string, any>>(
Expand Down Expand Up @@ -184,6 +197,19 @@ export const useFormHandler = <
}
}

const setValidating = (name: keyof TForm, validating: boolean) => {
console.log(validating)
if (formState.validating[name] !== validating) {
if (validating) {
formState.validating[name] = true
_updateValidatingState()
return
}
delete formState.validating[name]
_updateValidatingState()
}
}

const resetField = (name: keyof TForm) => {
values[name] = _getInitial(name)
setTouched(name, false)
Expand Down Expand Up @@ -319,6 +345,7 @@ export const useFormHandler = <
...(withDetails && {
isDirty: !!formState.dirty[name],
isTouched: !!formState.touched[name],
isValidating: !!formState.validating[name],
}),
...(native !== false && {
onChange: async () => {
Expand Down
55 changes: 54 additions & 1 deletion test/useFormHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { initialState, useFormHandler } from '@/useFormHandler'
import { expect, it, describe } from 'vitest'
import { retry } from './testing-utils'

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

describe('useFormHandler()', () => {
describe('register()', () => {
it('should register a field', () => {
Expand All @@ -12,6 +14,7 @@ describe('useFormHandler()', () => {
expect(field.onBlur).toBeDefined()
expect(field.isDirty).toBeUndefined()
expect(field.isTouched).toBeUndefined()
expect(field.isValidating).toBeUndefined()
expect(field.onClear).toBeDefined()
expect(field.onChange).toBeDefined()
expect(field.modelValue).toBe(null)
Expand All @@ -37,11 +40,12 @@ describe('useFormHandler()', () => {
expect(field.modelValue).toBeDefined()
expect(field['onUpdate:modelValue']).toBeDefined()
})
it('should apply dirty and touched states when withDetails is specified', () => {
it('should apply dirty, touched and validation states when withDetails is specified', () => {
const { register } = useFormHandler()
const field = register('field', { withDetails: true })
expect(field.isDirty).toBeDefined()
expect(field.isTouched).toBeDefined()
expect(field.isValidating).toBeDefined()
})
it('should apply default value', () => {
const { values, register } = useFormHandler()
Expand Down Expand Up @@ -142,6 +146,55 @@ describe('useFormHandler()', () => {
expect(values.field).toBe(null)
expect(formState.isDirty).toBeTruthy()
})
it('should set validating state when async validation is in progress', async () => {
const { register, formState, triggerValidation } = useFormHandler()
register('field', {
validate: {
asyncValidation: async (value: string) => {
await sleep(200)
return value === 'test' || 'error'
},
},
})
triggerValidation('field')
await sleep(20)
expect(formState.isValidating).toBeTruthy()
expect(formState.validating).toStrictEqual({ field: true })
await sleep(200)
expect(formState.isValidating).toBeFalsy()
expect(formState.validating).toStrictEqual({})
})
it('should correctly validate when async validation fails', async () => {
const { register, formState, triggerValidation } = useFormHandler()
register('field', {
validate: {
asyncValidation: async (value: string) => {
await sleep(200)
return value === 'test' || 'error'
},
},
})
triggerValidation('field')
await sleep(200)
expect(formState.errors.field).toBe('error')
expect(formState.isValid).toBeFalsy()
expect(formState.validating).toStrictEqual({})
expect(formState.isValidating).toBeFalsy()
})
it('should not set validating state for sync validation', async () => {
const { register, formState, triggerValidation } = useFormHandler()
register('field', {
validate: {
error: (value: string) => value === 'test' || 'error',
},
})
triggerValidation('field')
await sleep(10)
expect(formState.errors.field).toBe('error')
expect(formState.isValid).toBeFalsy()
expect(formState.isValidating).toBeFalsy()
expect(formState.validating).toStrictEqual({})
})
it('should set an error programmatically', async () => {
const { formState, setError } = useFormHandler()
setError('field', 'some error')
Expand Down
Loading