Skip to content

Instantly share code, notes, and snippets.

@Ryong-E
Last active March 10, 2024 13:04
Show Gist options
  • Save Ryong-E/b53575aba1be7689443387f631e89d9c to your computer and use it in GitHub Desktop.
Save Ryong-E/b53575aba1be7689443387f631e89d9c to your computer and use it in GitHub Desktop.
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,
};
}
export default function isEmptyObject(obj: object) {
return Object.keys(obj).length === 0;
}
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