Skip to content

Commit

Permalink
refactor: form context
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Apr 2, 2024
1 parent 5f72c6b commit 38e32cd
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 50 deletions.
19 changes: 19 additions & 0 deletions src/components/modules/comment/CommentBox/CommentBoxLegacyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ const placeholderMap = {
mail: '邮箱',
url: '网址',
} as const

const validatorMap = {
author: {
validator: (v: string) => v.length > 0 && v.length <= 20,
message: '昵称长度应在 1-20 之间',
},
mail: {
validator: (v: string) =>
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(v),
message: '邮箱格式不正确',
},
url: {
validator: (v: string) =>
/^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/.test(v),
message: '网址格式不正确',
},
}
const FormInput = (props: { fieldKey: FormKey; required?: boolean }) => {
const { fieldKey: key, required } = props
const [value, setValue] = useAtom(useGetCommentBoxAtomValues()[key])
Expand All @@ -34,7 +51,9 @@ const FormInput = (props: { fieldKey: FormKey; required?: boolean }) => {
onChange={(e) => setValue(e.target.value)}
required={required}
placeholder={placeholderMap[key] + (required ? ' *' : '')}
name={key}
className="border-0 bg-gray-200/50 dark:bg-zinc-800/50"
rules={[validatorMap[key]]}
/>
)
}
Expand Down
81 changes: 31 additions & 50 deletions src/components/ui/form/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,53 @@
import { memo, useCallback, useEffect, useRef } from 'react'
import { produce } from 'immer'
import { useAtomValue } from 'jotai'
import { selectAtom } from 'jotai/utils'
import { memo, useCallback, useRef } from 'react'
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from 'react'
import type { FormFieldBaseProps } from './types'

import { AutoResizeHeight } from '~/components/modules/shared/AutoResizeHeight'
import { clsxm } from '~/lib/helper'
import { jotaiStore } from '~/lib/store'

import { Input } from '../input'
import { useForm, useFormConfig } from './FormContext'
import { useFormConfig } from './FormContext'
import {
useAddField,
useCheckFieldStatus,
useFormErrorMessage,
useResetFieldStatus,
} from './hooks'

export const FormInput: FC<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> &
Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'name'
> &
FormFieldBaseProps<string>
> = memo(({ className, rules, onKeyDown, transform, ...rest }) => {
const FormCtx = useForm()
if (!FormCtx) throw new Error('FormInput must be used inside <FormContext />')
> = memo(({ className, rules, onKeyDown, transform, name, ...rest }) => {
const { showErrorMessage } = useFormConfig()
const { addField, removeField, fields } = FormCtx
const inputRef = useRef<HTMLInputElement>(null)

const errorMessage = useAtomValue(
selectAtom(
fields,
useCallback(
(atomValue) => {
if (!rest.name) return
return atomValue[rest.name]?.rules.find(
(rule) => rule.status === 'error',
)?.message
},
[rest.name],
),
),
)
useEffect(() => {
const name = rest.name
if (!rules) return
if (!name) return

addField(name, {
rules,
$ref: inputRef.current,
transform,
})
const inputRef = useRef<HTMLInputElement>(null)

return () => {
removeField(name)
}
}, [rest.name, rules, transform])
const errorMessage = useFormErrorMessage(name)
useAddField({
rules: rules || [],
transform,
$ref: inputRef.current,
name,
})
const resetFieldStatus = useResetFieldStatus(name)

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (onKeyDown) onKeyDown(e)
// const currentField =
jotaiStore.set(fields, (p) => {
return produce(p, (draft) => {
if (!rest.name) return
draft[rest.name].rules.forEach((rule) => {
if (rule.status === 'error') rule.status = 'success'
})
})
})
resetFieldStatus()
},
[fields, onKeyDown, rest.name],
[onKeyDown, resetFieldStatus],
)

const validateField = useCheckFieldStatus(name)

return (
<>
<Input
name={name}
ref={inputRef}
className={clsxm(
!!errorMessage && 'ring-2 ring-red-400 dark:ring-orange-700',
Expand All @@ -79,6 +56,10 @@ export const FormInput: FC<
)}
type="text"
onKeyDown={handleKeyDown}
onBlur={(e) => {
validateField()
rest.onBlur?.(e)
}}
{...rest}
/>

Expand Down
96 changes: 96 additions & 0 deletions src/components/ui/form/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useCallback, useEffect } from 'react'
import { produce } from 'immer'
import { useAtomValue, useStore } from 'jotai'
import { selectAtom } from 'jotai/utils'
import type { Field } from './types'

import { useForm } from './FormContext'

const useAssetFormContext = () => {
const FormCtx = useForm()
if (!FormCtx) throw new Error('FormInput must be used inside <FormContext />')
return FormCtx
}
export const useFormErrorMessage = (name: string) => {
const FormCtx = useAssetFormContext()

const { fields } = FormCtx

return useAtomValue(
selectAtom(
fields,
useCallback(
(atomValue) => {
if (!name) return
return atomValue[name]?.rules.find((rule) => rule.status === 'error')
?.message
},
[name],
),
),
)
}

export const useAddField = ({
rules,
transform,
$ref,
name,
}: Field & { name: string }) => {
const FormCtx = useAssetFormContext()

const { addField, removeField } = FormCtx
useEffect(() => {
if (!rules) return
if (!name) return

addField(name, {
rules,
$ref,
transform,
})

return () => {
removeField(name)
}
}, [$ref, addField, name, removeField, rules, transform])
}

export const useResetFieldStatus = (name: string) => {
const jotaiStore = useStore()
const FormCtx = useAssetFormContext()
const { fields } = FormCtx
return useCallback(() => {
jotaiStore.set(fields, (p) => {
return produce(p, (draft) => {
if (!name) return
draft[name].rules.forEach((rule) => {
if (rule.status === 'error') rule.status = 'success'
})
})
})
}, [fields, jotaiStore, name])
}

export const useCheckFieldStatus = (name: string) => {
const jotaiStore = useStore()
const FormCtx = useAssetFormContext()
const { fields } = FormCtx
return useCallback(() => {
jotaiStore.set(fields, (p) => {
return produce(p, (draft) => {
if (!name) return
const value = draft[name].$ref?.value
if (!value) return
draft[name].rules.some((rule) => {
const result = rule.validator(value)
if (!result) {
rule.status = 'error'

return true
}
})
})
})
}, [fields, jotaiStore, name])
}
9 changes: 9 additions & 0 deletions src/components/ui/form/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { DetailedHTMLProps, InputHTMLAttributes } from 'react'

export interface Rule<T = unknown> {
message: string
validator: (value: T) => boolean | Promise<boolean>
Expand All @@ -17,4 +19,11 @@ export interface Field {

export interface FormFieldBaseProps<T> extends Pick<Field, 'transform'> {
rules?: Rule<T>[]
name: string
}

export type InputFieldProps<T = string> = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'name'
> &
FormFieldBaseProps<T>

0 comments on commit 38e32cd

Please sign in to comment.