Skip to content

Commit

Permalink
feat(maz-ui): useFormValidator - expose the onBlur method
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisMazel committed Nov 29, 2024
1 parent 69172d2 commit cc66cff
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 33 deletions.
21 changes: 18 additions & 3 deletions packages/docs/docs/composables/use-form-validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const { model } = useFormValidator<Model>({

To use the `eager`, `blur`, or `progressive` validation modes, you must use the `useFormField` composable to add the necessary validation events.

Two ways to bind validation events:
3 ways to bind validation events:

##### 1. Use the `ref` attribute on the component to get the reference

Expand Down Expand Up @@ -104,7 +104,7 @@ const { value, errorMessage, isValid, hasError } = useFormField('name', {

You can use the `v-bind` directive to bind the validation events to the component or HTML element.

If you use this method with a custom component, the component must emit the `blur` event to trigger the field validation.
If you use this method with a custom component, the component must emit the `blur` event to trigger the field validation. Otherwise, use the first method.

```vue{7,16}
<template>
Expand All @@ -119,10 +119,25 @@ If you use this method with a custom component, the component must emit the `blu
<input v-model="value" v-bind="validationEvents" />
</template>
##### 3. Use the `onBlur` handler directly from `useFormField`
This method works if the component emits the `blur` event. Otherwise, use the first method.
```vue{7}
<template>
<MazInput
v-model="value"
:hint="errorMessage"
:error="hasError"
:success="isValid"
@blur="onBlur"
/>
</template>
<script setup lang="ts">
import { useFormField } from 'maz-ui'
const { value, errorMessage, isValid, hasError, validationEvents } = useFormField('name')
const { value, errorMessage, isValid, hasError, onBlur } = useFormField('name')
</script>
```

Expand Down
66 changes: 60 additions & 6 deletions packages/lib/modules/composables/useFormValidator/useFormField.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ComponentPublicInstance } from 'vue'
import type { ComponentPublicInstance, ComputedRef, WritableComputedRef } from 'vue'
import type {
BaseFormPayload,
ExtractModelKey,
FormFieldOptions,
FormSchema,
ValidationIssues,
} from './types'

import { computed, onMounted, onUnmounted } from 'vue'
Expand All @@ -24,10 +25,62 @@ import {
updateFieldState,
} from './utils'

interface UseFormFieldReturn<FieldType> {
/**
* Indicates if the field has an error
*/
hasError: ComputedRef<boolean>
/**
* Errors of the field
*/
errors: ComputedRef<ValidationIssues>
/**
* Error message of the field
* It's the first error of the field
*/
errorMessage: ComputedRef<string | undefined>
/**
* Indicates if the field is valid
*/
isValid: ComputedRef<boolean>
/**
* Indicates if the field has been modified
*/
isDirty: ComputedRef<boolean>
/**
* Indicates if the field has been blurred
*/
isBlurred: ComputedRef<boolean>
/**
* Indicates if the field has been validated
*/
isValidated: ComputedRef<boolean>
/**
* Indicates if the field is validating
*/
isValidating: ComputedRef<boolean>
/**
* Validation mode of the field
*/
mode: ComputedRef<FormFieldOptions<FieldType>['mode']>
/**
* Value of the field
*/
value: WritableComputedRef<FieldType>
/**
* Validation events of the field
*/
validationEvents: ComputedRef<ReturnType<typeof getValidationEvents>>
/**
* Function to handle the blur event of the field
*/
onBlur: () => void
}

export function useFormField<
FieldType extends Model[ExtractModelKey<FormSchema<Model>>],
Model extends BaseFormPayload = BaseFormPayload,
>(name: ExtractModelKey<FormSchema<Model>>, options?: FormFieldOptions<FieldType>) {
>(name: ExtractModelKey<FormSchema<Model>>, options?: FormFieldOptions<FieldType>): UseFormFieldReturn<FieldType> {
const opts = {
formIdentifier: 'main-form-validator',
...options,
Expand Down Expand Up @@ -72,7 +125,7 @@ export function useFormField<
})
}

function onBlurHandler() {
function onBlur() {
handleFieldBlur<Model>({
name,
fieldState: fieldState.value,
Expand All @@ -85,7 +138,7 @@ export function useFormField<
const validationEvents = computed(() =>
getValidationEvents({
ref: opts.ref,
onBlurHandler,
onBlur,
fieldState: fieldState.value,
}),
)
Expand All @@ -97,7 +150,7 @@ export function useFormField<
interactiveElements = findInteractiveElements(element)
addEventToInteractiveElements({
interactiveElements,
onBlurHandler,
onBlur,
mode: fieldMode,
})
}
Expand All @@ -118,7 +171,7 @@ export function useFormField<
onUnmounted(() => {
removeEventFromInteractiveElements({
interactiveElements,
onBlurHandler,
onBlur,
})
})
}
Expand All @@ -138,5 +191,6 @@ export function useFormField<
set: value => (payload.value[name] = value),
}),
validationEvents,
onBlur,
}
}
18 changes: 9 additions & 9 deletions packages/lib/modules/composables/useFormValidator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,29 +248,29 @@ export function findInteractiveElements(el: HTMLElement) {

export function addEventToInteractiveElements({
interactiveElements,
onBlurHandler,
onBlur,
mode,
}: {
interactiveElements: HTMLElement[]
onBlurHandler: () => void
onBlur: () => void
mode: StrictOptions['mode']
}) {
interactiveElements.forEach((element) => {
if (hasModeIncludes(['eager', 'blur', 'progressive'], mode)) {
element.addEventListener('blur', onBlurHandler)
element.addEventListener('blur', onBlur)
}
})
}

export function removeEventFromInteractiveElements({
interactiveElements,
onBlurHandler,
onBlur,
}: {
interactiveElements: HTMLElement[]
onBlurHandler: () => void
onBlur: () => void
}) {
interactiveElements.forEach((element) => {
element.removeEventListener('blur', onBlurHandler)
element.removeEventListener('blur', onBlur)
})
}

Expand Down Expand Up @@ -522,18 +522,18 @@ export function getContext<Model extends BaseFormPayload>(
export function getValidationEvents<Model extends BaseFormPayload>({
ref,
fieldState,
onBlurHandler,
onBlur,
}: {
ref?: string
fieldState: FieldState<Model>
onBlurHandler: () => void
onBlur: () => void
}) {
if (ref || hasModeIncludes(['aggressive', 'lazy'], fieldState.mode)) {
return
}

return {
onBlur: onBlurHandler,
onBlur,
}
}

Expand Down
30 changes: 15 additions & 15 deletions packages/lib/tests/specs/composables/useFormValidator/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,12 @@ describe('given addEventToInteractiveElements function', () => {
{ addEventListener: vi.fn(), getAttribute: vi.fn().mockReturnValue('text') },
{ addEventListener: vi.fn(), getAttribute: vi.fn().mockReturnValue('radio') },
] as unknown as HTMLElement[]
const onBlurHandler = vi.fn()
const onBlur = vi.fn()

addEventToInteractiveElements({ interactiveElements: mockElements, onBlurHandler, mode: 'eager' })
addEventToInteractiveElements({ interactiveElements: mockElements, onBlur, mode: 'eager' })

expect(mockElements[0].addEventListener).toHaveBeenCalledWith('blur', onBlurHandler)
expect(mockElements[1].addEventListener).toHaveBeenCalledWith('blur', onBlurHandler)
expect(mockElements[0].addEventListener).toHaveBeenCalledWith('blur', onBlur)
expect(mockElements[1].addEventListener).toHaveBeenCalledWith('blur', onBlur)
})
})
})
Expand All @@ -218,12 +218,12 @@ describe('given removeEventFromInteractiveElements function', () => {
describe('when called with interactive elements and event handlers', () => {
it('then it removes all event listeners from interactive elements', () => {
const mockElements = [{ removeEventListener: vi.fn() }, { removeEventListener: vi.fn() }] as unknown as HTMLElement[]
const onBlurHandler = vi.fn()
const onBlur = vi.fn()

removeEventFromInteractiveElements({ interactiveElements: mockElements, onBlurHandler })
removeEventFromInteractiveElements({ interactiveElements: mockElements, onBlur })

mockElements.forEach((element) => {
expect(element.removeEventListener).toHaveBeenCalledWith('blur', onBlurHandler)
expect(element.removeEventListener).toHaveBeenCalledWith('blur', onBlur)
})
})
})
Expand Down Expand Up @@ -511,43 +511,43 @@ describe('given getErrorMessages function', () => {
})

describe('given getValidationEvents function', () => {
const onBlurHandler = vi.fn()
const onBlur = vi.fn()

it('returns undefined when ref is provided', () => {
const result = getValidationEvents({
ref: 'test',
fieldState: { mode: 'eager' } as FieldState<{ name: string }>,
onBlurHandler,
onBlur,
})
expect(result).toBeUndefined()
})

it('returns undefined for aggressive or lazy mode', () => {
const result = getValidationEvents({
fieldState: { mode: 'aggressive' } as FieldState<{ name: string }>,
onBlurHandler,
onBlur,
})
expect(result).toBeUndefined()

const result2 = getValidationEvents({
fieldState: { mode: 'lazy' } as FieldState<{ name: string }>,
onBlurHandler,
onBlur,
})
expect(result2).toBeUndefined()
})

it('returns all events for other modes', () => {
const result = getValidationEvents({
fieldState: { mode: 'blur' } as FieldState<{ name: string }>,
onBlurHandler,
onBlur,
})
expect(result).toEqual({ onBlur: onBlurHandler })
expect(result).toEqual({ onBlur })

const result3 = getValidationEvents({
fieldState: { mode: 'eager' } as FieldState<{ name: string }>,
onBlurHandler,
onBlur,
})
expect(result3).toEqual({ onBlur: onBlurHandler })
expect(result3).toEqual({ onBlur })
})
})

Expand Down

0 comments on commit cc66cff

Please sign in to comment.