-
-
Save Ryong-E/b53575aba1be7689443387f631e89d9c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import isEmptyObject from '@/utils/isEmptyObject'; | |
import { ChangeEventHandler, Dispatch } from 'react'; | |
type FieldElement = | |
| HTMLInputElement | |
| HTMLSelectElement | |
| HTMLTextAreaElement | |
| (Partial<HTMLElement> & { | |
type?: string; | |
name: string; | |
value?: any; | |
checked?: boolean; | |
}); | |
type Ref = FieldElement; | |
export type RegisterOption = Partial<{ | |
validate: Validate; | |
onChange: ChangeEventHandler; | |
required: boolean; | |
disalbed: boolean; | |
}>; | |
export type UseFormProps<TFieldValues> = Partial<{ | |
defaultValues: TFieldValues; | |
onStateChange?: Dispatch<FormState>; | |
}>; | |
export type UseFormReturn = { | |
register: UseFormRegister; | |
handleSubmit: SubmitEvent; | |
formState: FormState; | |
setValue: (name: string, value: any) => void; | |
}; | |
type FieldError = Record<string, Error>; | |
type Field = { | |
_f: { | |
ref: Ref; | |
refs?: Ref[]; | |
name: string; | |
} & RegisterOption; | |
}; | |
type FieldRefs = Partial<Record<string, Field>>; | |
export type FieldValues = Record<string, any>; | |
type FormState = { | |
errors: FieldError; | |
isDirty: boolean; | |
}; | |
type RefCallback = (instance: any) => void; | |
type InterString = string; | |
type UseFormRegisterReturn<TFieldName extends InterString = InterString> = { | |
onChange: ChangeEventHandler; | |
ref: RefCallback; | |
name: TFieldName; | |
required?: boolean; | |
}; | |
type UseFormRegister = ( | |
name: string, | |
options?: RegisterOption, | |
) => UseFormRegisterReturn; | |
type ChangeHandler = (event: { | |
target: any; | |
type?: any; | |
}) => Promise<void | boolean>; | |
type SubmitHandler = ( | |
data: any, | |
event?: React.FormEvent, | |
) => unknown | Promise<unknown>; | |
type SubmitEvent = ( | |
onValid: SubmitHandler, | |
) => (e?: React.FormEvent) => Promise<void>; | |
type Error = { | |
message: string; | |
ref: Ref; | |
type: string; | |
}; | |
type ValidateFunction = (value: any) => boolean; | |
type ValidateWithMessage = { | |
value: ValidateFunction; | |
message: string; | |
}; | |
type Validate = ValidateFunction | Record<string, ValidateWithMessage>; | |
export default function createForm< | |
TFieldValues extends FieldValues = FieldValues, | |
>(props: UseFormProps<TFieldValues> = {}): Omit<UseFormReturn, 'formState'> { | |
const _options: UseFormProps<TFieldValues> = { | |
...props, | |
}; | |
const _formValue: FieldValues = {...props.defaultValues}; | |
const _fields: FieldRefs = {}; | |
const _formState: FormState = { | |
errors: {}, | |
isDirty: false, | |
}; | |
const getFieldValue = (_f: Field['_f']) => { | |
const { ref } = _f; | |
if (ref.type === 'checkbox' || ref.type === 'radio') { | |
const value: (string | boolean)[] = []; | |
const refs = _f.refs as HTMLInputElement[]; | |
refs.forEach((_ref) => { | |
if (_ref.checked) { | |
value.push(_ref.value ?? true); | |
} | |
}); | |
return value.length > 1 ? value : value[0]; | |
} | |
return ref.value; | |
}; | |
const setError = (name: string, value: Error) => { | |
if (name in _formState.errors) { | |
return; | |
} | |
_formState.errors[name] = value; | |
}; | |
const onChange: ChangeHandler = async (event) => { | |
const target = event.target; | |
const name = target.name; | |
const field = _fields[name]; | |
if (_formState.errors[name]) { | |
delete _formState.errors[name]; | |
} | |
if (field?._f.onChange) { | |
field._f.onChange(event.target.value); | |
} | |
if (field) { | |
const fieldValue = getFieldValue(field._f); | |
_formValue[name] = fieldValue; | |
} | |
}; | |
const handleSubmit: SubmitEvent = (onValid) => async (e) => { | |
if (e) { | |
if (e.preventDefault) { | |
e.preventDefault(); | |
} | |
} | |
const fieldValue = _formValue; | |
Object.entries(_fields).forEach(([key, value]) => { | |
if (value?._f.required) { | |
if ( | |
fieldValue[key] === false || | |
fieldValue[key] === '' || | |
fieldValue[key] === undefined | |
) { | |
const error = { | |
type: 'required', | |
message: '필수값입니다', | |
ref: _fields[key]!._f.refs | |
? _fields[key]!._f.refs![0] | |
: _fields[key]!._f.ref, | |
}; | |
setError(key, error); | |
return; | |
} | |
delete _formState.errors[key]; | |
} | |
if (value?._f.validate) { | |
if (typeof value._f.validate === 'function') { | |
if (!value._f.validate(fieldValue[key])) { | |
const error = { | |
type: key, | |
message: '값을 확인해주세요', | |
ref: _fields[key]!._f.refs | |
? _fields[key]!._f.refs![0] | |
: _fields[key]!._f.ref, | |
}; | |
setError(key, error); | |
} else { | |
delete _formState.errors[key]; | |
} | |
} | |
if (typeof value._f.validate === 'object') { | |
Object.entries(value._f.validate).forEach(([fnName, fn]) => { | |
if (!fn.value(fieldValue[key])) { | |
const error = { | |
type: fnName, | |
message: fn.message, | |
ref: _fields[key]!._f.refs | |
? _fields[key]!._f.refs![0] | |
: _fields[key]!._f.ref, | |
}; | |
setError(key, error); | |
return; | |
} | |
delete _formState.errors[key]; | |
}); | |
} | |
} | |
}); | |
if (_options.onStateChange) { | |
_options.onStateChange({ ..._formState }); | |
} | |
if (isEmptyObject(_formState.errors)) { | |
await onValid({ ...fieldValue }, e); | |
} | |
}; | |
const setValue = (name: string, value: any) => { | |
const field = _fields[name]; | |
if (field) { | |
field._f.ref.value = value; | |
_formValue[name] = value; | |
} | |
}; | |
const register: UseFormRegister = (name, options = {}) => { | |
let field = _fields[name]; | |
if (field) { | |
const newOption = { | |
_f: { | |
...(field && field._f ? field._f : { ref: { name } }), | |
name, | |
...options, | |
}, | |
}; | |
_fields[name] = newOption; | |
} | |
return { | |
...(options.disalbed | |
? { disabled: options.disalbed } | |
: { disabled: false }), | |
name, | |
onChange, | |
ref: (ref: HTMLInputElement | null): void => { | |
if (ref) { | |
register(name, options); | |
field = _fields[name]; | |
const fieldRef = | |
typeof ref.value === 'undefined' | |
? ref.querySelectorAll | |
? (ref.querySelectorAll('input,select,textarea')[0] as Ref) || | |
ref | |
: ref | |
: ref; | |
const radioOrCheckbox = | |
ref.type === 'radio' || ref.type === 'checkbox'; | |
const refs = field?._f.refs || []; | |
if ( | |
radioOrCheckbox | |
? refs.find((option: Ref) => option === fieldRef) | |
: fieldRef === field?._f.ref | |
) { | |
return; | |
} | |
if (_formValue[name]) { | |
if (ref.type === 'radio' || ref.type === 'checkbox') { | |
if (Array.isArray(_formValue[name])) { | |
if (_formValue[name].includes(ref.value)) { | |
ref.checked = true; | |
} | |
} else if (ref.value === _formValue[name]) { | |
ref.click(); | |
ref.checked = true; | |
} | |
} else { | |
ref.value = _formValue[name]; | |
} | |
} | |
const newField = { | |
_f: { | |
...field?._f, | |
name, | |
...options, | |
...(radioOrCheckbox | |
? { | |
refs: [ | |
...refs.filter((value) => value instanceof Element), | |
fieldRef as HTMLInputElement, | |
], | |
ref: { type: fieldRef.type, name }, | |
} | |
: { ref: fieldRef }), | |
}, | |
}; | |
_fields[name] = newField; | |
} | |
}, | |
}; | |
}; | |
return { | |
register, | |
handleSubmit, | |
setValue, | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function isEmptyObject(obj: object) { | |
return Object.keys(obj).length === 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import createForm, { | |
FieldValues, | |
UseFormProps, | |
UseFormReturn, | |
} from '@/lib/form/createForm'; | |
import { useRef, useState } from 'react'; | |
export function useForm<TFieldValues extends FieldValues = FieldValues>( | |
props: UseFormProps<TFieldValues> = {}, | |
): UseFormReturn { | |
const _formControl = useRef<UseFormReturn | undefined>(); | |
const [formState, setFormState] = useState({ | |
errors: {}, | |
isDirty: false, | |
}); | |
if (!_formControl.current) { | |
const formMethods = createForm<TFieldValues>({ | |
...props, | |
onStateChange: setFormState, | |
}); | |
_formControl.current = { | |
...formMethods, | |
formState, | |
}; | |
} | |
return { ..._formControl.current, formState }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment