next-gs
Version:
NPM package for building a React+NextJS+Prisma admin application.
209 lines (171 loc) • 5.04 kB
text/typescript
import React from "react";
import { shallowEqual } from "react-redux";
import { useMerge } from "./useMerge";
import {
fn,
type JsonValue,
FormContext,
type FormSubmitResult,
type FormErrors,
type FormInputOptions,
type FormInputState,
type FormState,
type FormValues,
type FormFieldOptions,
type FormFields,
type ValidatorFunc,
} from "@next-gs/client";
export type FormChangeHandler<FV extends FormValues> = (values: FV, field?: string) => FV | void;
export type UseFormProps<FV extends FormValues> = {
values: FV;
onSubmit?: (values: FV) => FormSubmitResult<FV>;
onValidate?: (values: FV) => FormSubmitResult<FV>;
onChange?: FormChangeHandler<FV>;
};
export interface UseFormReturn<FV extends FormValues> extends FormState<FV> {
reset: (newValues?: FV) => void;
submit: () => FormSubmitResult<FV>;
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
registerInput: (props: FormInputOptions) => FormInputState;
}
export const Validators = {
required: ((val, more) => undefined) as ValidatorFunc,
};
export function useForm<FV extends FormValues>({
values: initialValues,
onSubmit,
onValidate,
onChange,
}: UseFormProps<FV>) {
const [{ values, valid, submitting }, setState] = useMerge<
FormState<FV>
>({
values: initialValues || {},
valid: false,
submitting: false,
});
const fields = React.useRef({} as FormFields<FV>);
const setErrors = (errors: FormErrors<FV>) => {
const newFields = { ...fields.current };
Object.keys(newFields).forEach((k) => {
newFields[k].error = errors[k];
});
fields.current = newFields;
};
const checkField = (field: FormFieldOptions, value?: JsonValue) => {
if (!field) return;
let error: string | undefined = undefined;
if (field.required) error = Validators.required(value);
if (!error && fn.isFunction(field.validate))
error = field.validate(value, values);
return { ...field, error };
};
const change = (name: string, value: JsonValue, others: FormValues) => {
const field = fields.current[name];
if (!field || field.readonly) return;
if (shallowEqual(values[name], value)) return;
const newFields = { ...fields.current, [name]: checkField(field, value) };
let newValues: FV = fn.assign({}, values, others, { [name]: value });
const error = fn.find(newFields, field => fn.notEmpty(field.error));
if (onChange) {
newValues = onChange(newValues, name) || values;
}
setState({
values: newValues as FV,
valid: !error,
});
};
const submit = async () => {
setState({ submitting: true });
try {
if (onValidate) {
const result = await onValidate(values);
if (result) {
setErrors(result);
return;
}
}
if (onSubmit) {
const result = await onSubmit(values);
if (fn.isObject(result)) setErrors(result);
}
} catch (error) {
console.error(error);
} finally {
setState({ submitting: false });
}
};
const reset = (newValues: FV = initialValues) =>
setState({ values: newValues, valid: false });
const registerField = ({
name,
required,
readonly,
initial,
validate,
}: FormInputOptions) => {
const field: FormFieldOptions = {
name,
required,
readonly,
validate,
touched: false,
initial
};
fields.current = { ...fields.current, [name]: field };
return field;
};
const registerInput = (input: FormInputOptions) => {
const {
name,
label,
required,
initial,
disabled = false,
parse,
format,
...inputRest
} = input;
let field = fields.current[name];
if (!field) field = registerField(input);
const { error, touched } = field;
let value = name in values ? values[name] : initial;
if (format) value = format(value);
return {
name,
label,
error: touched ? error : undefined,
disabled: fn.resolve(disabled, values),
value,
required,
onChange: (value: JsonValue) => {
change(name, parse ? parse(value) : value, values);
},
onFocus: () => {
fields.current = { ...fields.current, [name]: { ...field, touched: true } };
},
onBlur: () => {
fields.current = { ...fields.current, [name]: checkField(field) };
},
...inputRest
} as FormInputState;
};
return {
fields,
values,
submitting,
valid,
submit,
reset,
change,
handleSubmit: (e) => {
e.preventDefault();
submit();
},
registerField,
registerInput,
} as UseFormReturn<FV>;
}
export function useFormContext() {
return React.useContext(FormContext) as UseFormReturn<FormValues>;
}