UNPKG

react-hook-formify

Version:

A smart wrapper around react-hook-form + zustand

203 lines (195 loc) 7.97 kB
// src/components/form.tsx import { forwardRef, useEffect } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { yupResolver } from "@hookform/resolvers/yup"; // ../../../node_modules/zustand/esm/vanilla.mjs var createStoreImpl = (createState) => { let state; const listeners = /* @__PURE__ */ new Set(); const setState = (partial, replace) => { const nextState = typeof partial === "function" ? partial(state) : partial; if (!Object.is(nextState, state)) { const previousState = state; state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState); listeners.forEach((listener) => listener(state, previousState)); } }; const getState = () => state; const getInitialState = () => initialState; const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; const api = { setState, getState, getInitialState, subscribe }; const initialState = state = createState(setState, getState, api); return api; }; var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl); // ../../../node_modules/zustand/esm/react.mjs import React from "react"; var identity = (arg) => arg; function useStore(api, selector = identity) { const slice = React.useSyncExternalStore( api.subscribe, React.useCallback(() => selector(api.getState()), [api, selector]), React.useCallback(() => selector(api.getInitialState()), [api, selector]) ); React.useDebugValue(slice); return slice; } var createImpl = (createState) => { const api = createStore(createState); const useBoundStore = (selector) => useStore(api, selector); Object.assign(useBoundStore, api); return useBoundStore; }; var create = ((createState) => createState ? createImpl(createState) : createImpl); // src/store/form.store.ts var useFormStore = create((set) => ({ formData: {}, setFormData: (key, data) => set((state) => ({ formData: { ...state.formData, [key]: data } })), resetForm: () => set({ formData: {} }) })); // src/utils/debounce.ts function debounce(func, wait) { let timeout; const debounced = (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; debounced.cancel = () => clearTimeout(timeout); return debounced; } // src/utils/validation.ts import * as yup from "yup"; var generateSchema = (fields = [], t) => { const shape = {}; const getBaseValidator = (field) => { if (typeof field?.type === "function") return field?.type(yup); switch (field?.type) { case "email": return yup.string().email(t("validation.email")); case "number": return yup.number().transform((v, o) => o === "" ? null : v).nullable().typeError(t("validation.number")); case "object": return yup.object().nullable().transform((value, originalValue) => { return originalValue === "" ? null : value; }); case "array": return yup.array().nullable().transform((value, originalValue) => { return originalValue === "" ? null : value; }); case "file": return yup.mixed().nullable().transform((value, originalValue) => { return originalValue === "" ? null : value; }).test("fileType", t("validation.invalid"), (value) => { if (value == null) return true; if (value instanceof File) return true; if (Array.isArray(value) && value.length > 0) return true; return typeof value === "object" && Object.keys(value).length > 0; }); case "boolean": return yup.boolean().typeError(t("validation.boolean")); case "date": return yup.date().nullable().transform((v, o) => o === "" ? null : v).typeError(t("validation.date")); case "string": default: return yup.string(); } }; const applyRules = (schema, rules) => { if (rules.required) schema = schema.required(t("validation.required")); if (rules.min !== void 0) schema = schema.min?.(rules.min, t("validation.min", { min: rules.min })) ?? schema; if (rules.max !== void 0) schema = schema.max?.(rules.max, t("validation.max", { max: rules.max })) ?? schema; if (rules.matches) schema = schema.matches?.(rules.matches.regex, t(rules.matches.message || "validation.invalid")) ?? schema; return schema; }; fields.forEach((field) => { if (field?.when && (field.when.field || Array.isArray(field.when.fields))) { const whenFields = Array.isArray(field.when.fields) ? field.when.fields : [field.when.field]; const conditionFn = field.when.is; const thenRules = field.when.then || {}; const otherwiseRules = field.when.otherwise || {}; shape[field.name] = yup.mixed().when(whenFields, (...args) => { const values = args.slice(0, whenFields.length); const base = getBaseValidator(field); try { const condition = typeof conditionFn === "function" ? conditionFn(...values) : conditionFn === values[0]; return condition ? applyRules(base, thenRules) : applyRules(base, otherwiseRules); } catch (err) { console.warn(`generateSchema: conditionFn error in field "${field.name}"`, err); return base; } }); } else { shape[field.name] = applyRules(getBaseValidator(field), field); } }); return yup.object().shape(shape); }; // src/components/form.tsx import { jsx } from "react/jsx-runtime"; var FormComponent = forwardRef(({ name = "form", children, onSubmit, enableStore, isLoading = false, fields = [] }, ref) => { const { t } = useTranslation(); const initialValues = Object.fromEntries(fields.map((f) => [f.name, f.value ?? ""])); const methods = useForm({ defaultValues: initialValues, values: initialValues, resolver: yupResolver(generateSchema(fields, t)), mode: "all", shouldFocusError: false, reValidateMode: "onChange" }); useEffect(() => { if (!enableStore) return; const debouncedUpdate = debounce((values) => { useFormStore.getState().setFormData(name, values); }, 300); const subscription = methods.watch((values) => { debouncedUpdate(values); }); return () => { subscription.unsubscribe(); debouncedUpdate.cancel(); }; }, [enableStore, methods, methods.watch, name]); const handleSubmit = async (values) => { const isValid = await methods.trigger(); if (isValid) { const renderedValues = Object.fromEntries( fields.map((field) => { const rawValue = values[field.name]; const transformed = typeof field.onSubmitValue === "function" ? field.onSubmitValue(rawValue) : rawValue; return [field.name, transformed]; }) ); onSubmit?.({ values: renderedValues, ...methods }); } else { console.log("Form error"); } }; return /* @__PURE__ */ jsx(FormProvider, { ...methods, children: /* @__PURE__ */ jsx("form", { ref, onSubmit: methods.handleSubmit(handleSubmit), children: typeof children === "function" ? children({ ...methods, isLoading, values: methods.watch(), errors: methods.formState.errors }) : children }) }); }); var form_default = FormComponent; // src/components/field.tsx import { Controller, useFormContext } from "react-hook-form"; import { jsx as jsx2 } from "react/jsx-runtime"; var FieldComponent = ({ name, component, ...rest }) => { const { control, getFieldState } = useFormContext(); const Component = component || null; return /* @__PURE__ */ jsx2(Controller, { name, control, render: ({ field }) => /* @__PURE__ */ jsx2(Component, { ...getFieldState(name), ...rest, ...field }) }); }; var field_default = FieldComponent; export { field_default as Field, form_default as Form, useFormStore };