refactor: usage of useForm
hook from react-component-form
This commit is contained in:
22
hooks/useClickOutsideAlerter.ts
Normal file
22
hooks/useClickOutsideAlerter.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const useClickOutsideAlerter = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: () => void
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
if (
|
||||
event.target != null &&
|
||||
ref.current != null &&
|
||||
!ref.current.contains(event.target as Node)
|
||||
) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [ref, callback])
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export const fetchState = ['idle', 'loading', 'error', 'success'] as const
|
||||
|
||||
export type FetchState = typeof fetchState[number]
|
||||
|
||||
export const useFetchState = (
|
||||
initialFetchState: FetchState = 'idle'
|
||||
): [
|
||||
fetchState: FetchState,
|
||||
setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
|
||||
] => {
|
||||
const [fetchState, setFetchState] = useState<FetchState>(initialFetchState)
|
||||
return [fetchState, setFetchState]
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import type { ErrorObject } from 'ajv'
|
||||
|
||||
const knownErrorKeywords = ['minLength', 'maxLength', 'format']
|
||||
|
||||
export const getErrorTranslationKey = (error: ErrorObject): string => {
|
||||
if (knownErrorKeywords.includes(error?.keyword)) {
|
||||
if (error.keyword === 'minLength' && error.params.limit === 1) {
|
||||
return 'errors:required'
|
||||
}
|
||||
if (error.keyword === 'format') {
|
||||
if (error.params.format === 'email') {
|
||||
return 'errors:email'
|
||||
}
|
||||
return 'errors:invalid'
|
||||
}
|
||||
return `errors:${error.keyword}`
|
||||
}
|
||||
return 'errors:invalid'
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import type { TObject } from '@sinclair/typebox'
|
||||
|
||||
import type { ObjectAny } from '../../tools/types'
|
||||
|
||||
export const handleCheckboxBoolean = (
|
||||
object: ObjectAny,
|
||||
validateSchemaObject: TObject<ObjectAny>
|
||||
): ObjectAny => {
|
||||
const booleanProperties: string[] = []
|
||||
for (const property in validateSchemaObject.properties) {
|
||||
const rule = validateSchemaObject.properties[property]
|
||||
if (rule.type === 'boolean') {
|
||||
booleanProperties.push(property)
|
||||
}
|
||||
}
|
||||
for (const booleanProperty of booleanProperties) {
|
||||
if (object[booleanProperty] == null) {
|
||||
object[booleanProperty] =
|
||||
validateSchemaObject.properties[booleanProperty].default
|
||||
} else {
|
||||
object[booleanProperty] = object[booleanProperty] === 'on'
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './useForm'
|
@ -1,19 +0,0 @@
|
||||
import type { ObjectAny } from '../../tools/types'
|
||||
|
||||
export const replaceEmptyStringInObjectToNull = (
|
||||
object: ObjectAny,
|
||||
required: string[] = []
|
||||
): ObjectAny => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(object).map(([key, value]) => {
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
value.length === 0 &&
|
||||
!required.includes(key)
|
||||
) {
|
||||
return [key, null]
|
||||
}
|
||||
return [key, value]
|
||||
})
|
||||
)
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { HandleForm } from 'react-component-form'
|
||||
import type { ErrorObject } from 'ajv'
|
||||
|
||||
import { FetchState, useFetchState } from '../useFetchState'
|
||||
import { ajv } from '../../tools/ajv'
|
||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
|
||||
import { replaceEmptyStringInObjectToNull } from './replaceEmptyStringInObjectToNull'
|
||||
import type { ObjectAny } from '../../tools/types'
|
||||
import { handleCheckboxBoolean } from './handleCheckboxBoolean'
|
||||
|
||||
interface Errors {
|
||||
[key: string]: ErrorObject<string, any> | null | undefined
|
||||
}
|
||||
|
||||
const findError = (
|
||||
field: string
|
||||
): ((value: ErrorObject, index: number, object: ErrorObject[]) => boolean) => {
|
||||
return (validationError) => validationError.instancePath === field
|
||||
}
|
||||
|
||||
export type GetErrorTranslation = (error?: ErrorObject | null) => string | null
|
||||
|
||||
export interface UseFormOptions {
|
||||
validateSchema: { [key: string]: any }
|
||||
replaceEmptyStringToNull?: boolean
|
||||
resetOnSuccess?: boolean
|
||||
}
|
||||
|
||||
export type HandleSubmit = (callback: HandleSubmitCallback) => HandleForm
|
||||
|
||||
interface Message {
|
||||
type: 'error' | 'success'
|
||||
value: string
|
||||
}
|
||||
|
||||
export type HandleSubmitCallback = (
|
||||
formData: ObjectAny,
|
||||
formElement: HTMLFormElement
|
||||
) => Promise<Message | null>
|
||||
|
||||
export interface UseFormResult {
|
||||
message: string | null
|
||||
setMessageTranslationKey: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>
|
||||
fetchState: FetchState
|
||||
setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
|
||||
getErrorTranslation: GetErrorTranslation
|
||||
handleSubmit: HandleSubmit
|
||||
errors: Errors
|
||||
}
|
||||
|
||||
export const useForm = (options: UseFormOptions): UseFormResult => {
|
||||
const {
|
||||
validateSchema,
|
||||
replaceEmptyStringToNull = false,
|
||||
resetOnSuccess = false
|
||||
} = options
|
||||
const { t } = useTranslation()
|
||||
const [fetchState, setFetchState] = useFetchState()
|
||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [errors, setErrors] = useState<Errors>({})
|
||||
|
||||
const validateSchemaObject = useMemo(() => {
|
||||
return Type.Object(validateSchema)
|
||||
}, [validateSchema])
|
||||
|
||||
const validate = useMemo(() => {
|
||||
return ajv.compile(validateSchemaObject)
|
||||
}, [validateSchemaObject])
|
||||
|
||||
const getErrorTranslation = (error?: ErrorObject | null): string | null => {
|
||||
if (error != null) {
|
||||
return t(getErrorTranslationKey(error)).replace(
|
||||
'{expected}',
|
||||
error?.params?.limit
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit: HandleSubmit = (callback) => {
|
||||
return async (formData: ObjectAny, formElement) => {
|
||||
setErrors({})
|
||||
setMessageTranslationKey(undefined)
|
||||
if (replaceEmptyStringToNull) {
|
||||
formData = replaceEmptyStringInObjectToNull(
|
||||
formData,
|
||||
validateSchemaObject.required
|
||||
)
|
||||
}
|
||||
formData = handleCheckboxBoolean(formData, validateSchemaObject)
|
||||
const isValid = validate(formData)
|
||||
if (!isValid) {
|
||||
setFetchState('error')
|
||||
const errors: Errors = {}
|
||||
for (const property in validateSchemaObject.properties) {
|
||||
errors[property] = validate.errors?.find(findError(`/${property}`))
|
||||
}
|
||||
setErrors(errors)
|
||||
} else {
|
||||
setErrors({})
|
||||
setFetchState('loading')
|
||||
const message = await callback(formData, formElement)
|
||||
if (message != null) {
|
||||
setMessageTranslationKey(message.value)
|
||||
if (message.type === 'success') {
|
||||
setFetchState('success')
|
||||
if (resetOnSuccess) {
|
||||
formElement.reset()
|
||||
}
|
||||
} else {
|
||||
setFetchState('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getErrorTranslation,
|
||||
errors,
|
||||
fetchState,
|
||||
setFetchState,
|
||||
handleSubmit,
|
||||
message: messageTranslationKey != null ? t(messageTranslationKey) : null,
|
||||
setMessageTranslationKey
|
||||
}
|
||||
}
|
60
hooks/useFormTranslation.ts
Normal file
60
hooks/useFormTranslation.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import type { Error } from 'react-component-form'
|
||||
|
||||
const knownErrorKeywords = ['minLength', 'maxLength', 'format']
|
||||
|
||||
const getErrorTranslationKey = (error: Error): string => {
|
||||
if (knownErrorKeywords.includes(error?.keyword)) {
|
||||
if (
|
||||
error.keyword === 'minLength' &&
|
||||
typeof error.data === 'string' &&
|
||||
error.data.length === 0
|
||||
) {
|
||||
return 'errors:required'
|
||||
}
|
||||
if (error.keyword === 'format') {
|
||||
if (error.params.format === 'email') {
|
||||
return 'errors:invalid-email'
|
||||
}
|
||||
return 'errors:invalid'
|
||||
}
|
||||
return `errors:${error.keyword}`
|
||||
}
|
||||
return 'errors:invalid'
|
||||
}
|
||||
|
||||
export type GetErrorTranslation = (
|
||||
error: Error | undefined
|
||||
) => string | undefined
|
||||
|
||||
export type GetFirstErrorTranslation = (
|
||||
errors: Error[] | undefined
|
||||
) => string | undefined
|
||||
|
||||
export interface UseFormTranslationResult {
|
||||
getErrorTranslation: GetErrorTranslation
|
||||
getFirstErrorTranslation: GetFirstErrorTranslation
|
||||
}
|
||||
|
||||
export const useFormTranslation = (): UseFormTranslationResult => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getErrorTranslation: GetErrorTranslation = (error) => {
|
||||
if (error != null) {
|
||||
return t(getErrorTranslationKey(error)).replace(
|
||||
'{expected}',
|
||||
error?.params?.limit
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getFirstErrorTranslation: GetFirstErrorTranslation = (errors) => {
|
||||
if (errors != null) {
|
||||
return getErrorTranslation(errors[0])
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { getFirstErrorTranslation, getErrorTranslation }
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { useState, useRef, useCallback } from 'react'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import type { FetchState } from 'react-component-form'
|
||||
|
||||
import { FetchState } from './useFetchState'
|
||||
import {
|
||||
CacheKey,
|
||||
getPaginationCache,
|
||||
|
Reference in New Issue
Block a user