react-vite-demo
react and vite demo
git clone https://9o.is/git/react-vite-demo.git
Form.tsx
(2007B)
1 import { createContext, ReactNode, useContext, useRef, useState } from "react"
2
3 type FormProps<T> = ({
4 children: ReactNode
5 ['aria-label']?: string
6 }) & ({
7 onSubmit?: (data: FormDataMap) => void
8 validator: undefined
9 } | {
10 onSubmit: (data: T) => void
11 validator: (data: FormDataMap) => T
12 })
13
14 export type FormDataMap = {
15 [key: string]: FormDataEntryValue | undefined
16 }
17
18 type FormContextProps = {
19 formError?: string
20 }
21
22 const FormContext = createContext<FormContextProps | undefined>(undefined)
23
24 export function Form<T>({ onSubmit, validator, children, ...props }: FormProps<T>) {
25 const formRef = useRef<HTMLFormElement>(null)
26 const [formError, setFormError] = useState<string | undefined>()
27
28 const internalOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
29 event.preventDefault()
30
31 const formData = new FormData(formRef.current || undefined)
32 const data: FormDataMap = {}
33
34 for (const [k, v] of formData.entries()) {
35 data[k] = v
36 }
37
38 if (validator) {
39 try {
40 onSubmit(validator(data))
41 } catch (error) {
42 setFormError((error as Error).message)
43 }
44 return
45 }
46
47 onSubmit?.(data)
48 }
49
50 return (
51 <FormContext.Provider value={{ formError }}>
52 <form onSubmit={internalOnSubmit} ref={formRef} {...props}>
53 {children}
54 </form>
55 </FormContext.Provider>
56 )
57 }
58
59 type FormErrorProps = {
60 children: (error: string) => ReactNode
61 }
62
63 function FormError({ children }: FormErrorProps) {
64 const { formError } = useFormContext()
65
66 return (
67 <div role="alert">
68 {formError ? children(formError) : <> </>}
69 </div>
70 )
71 }
72
73 function useFormContext() {
74 const formContext = useContext(FormContext)
75 if (!formContext) throw new Error('useFormContext() must be used within a Form')
76 return formContext
77 }
78
79 Form.Error = FormError