UNPKG

remix-validated-form

Version:

Form component and utils for easy form validation in remix

119 lines (118 loc) 6.97 kB
import { useActionData, useMatches, useTransition } from "@remix-run/react"; import { useCallback, useContext } from "react"; import { getPath } from "set-get"; import invariant from "tiny-invariant"; import { formDefaultValuesKey } from "./constants"; import { InternalFormContext } from "./formContext"; import { hydratable } from "./hydratable"; import { useFormStore } from "./state/storeHooks"; export const useInternalFormContext = (formId, hookName) => { const formContext = useContext(InternalFormContext); if (formId) return { formId }; if (formContext) return formContext; throw new Error(`Unable to determine form for ${hookName}. Please use it inside a ValidatedForm or pass a 'formId'.`); }; export function useErrorResponseForForm({ fetcher, subaction, formId, }) { var _a; const actionData = useActionData(); if (fetcher) { if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors) return fetcher.data; return null; } if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors)) return null; // If there's an explicit id, we should ignore data that has the wrong id if (typeof formId === "string" && actionData.formId) return actionData.formId === formId ? actionData : null; if ((!subaction && !actionData.subaction) || actionData.subaction === subaction) return actionData; return null; } export const useFieldErrorsForForm = (context) => { const response = useErrorResponseForForm(context); const hydrated = useFormStore(context.formId, (state) => state.isHydrated); return hydratable.from(response === null || response === void 0 ? void 0 : response.fieldErrors, hydrated); }; export const useDefaultValuesFromLoader = ({ formId, }) => { const matches = useMatches(); if (typeof formId === "string") { const dataKey = formDefaultValuesKey(formId); // If multiple loaders declare the same default values, // we should use the data from the deepest route. const match = matches .reverse() .find((match) => match.data && dataKey in match.data); return match === null || match === void 0 ? void 0 : match.data[dataKey]; } return null; }; export const useDefaultValuesForForm = (context) => { const { formId, defaultValuesProp } = context; const hydrated = useFormStore(formId, (state) => state.isHydrated); const errorResponse = useErrorResponseForForm(context); const defaultValuesFromLoader = useDefaultValuesFromLoader(context); // Typical flow is: // - Default values only available from props or server // - Props have a higher priority than server // - State gets hydrated with default values // - After submit, we may need to use values from the error if (hydrated) return hydratable.hydratedData(); if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields) { invariant(typeof errorResponse.repopulateFields === "object", "repopulateFields returned something other than an object"); return hydratable.serverData(errorResponse.repopulateFields); } if (defaultValuesProp) return hydratable.serverData(defaultValuesProp); return hydratable.serverData(defaultValuesFromLoader); }; export const useHasActiveFormSubmit = ({ fetcher, }) => { const transition = useTransition(); const hasActiveSubmission = fetcher ? fetcher.state === "submitting" : !!transition.submission; return hasActiveSubmission; }; export const useFieldTouched = (field, { formId }) => { const touched = useFormStore(formId, (state) => state.touchedFields[field]); const setFieldTouched = useFormStore(formId, (state) => state.setTouched); const setTouched = useCallback((touched) => setFieldTouched(field, touched), [field, setFieldTouched]); return [touched, setTouched]; }; export const useFieldError = (name, context) => { const fieldErrors = useFieldErrorsForForm(context); const state = useFormStore(context.formId, (state) => state.fieldErrors[name]); return fieldErrors.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]).hydrateTo(state); }; export const useClearError = (context) => { const { formId } = context; return useFormStore(formId, (state) => state.clearFieldError); }; export const useCurrentDefaultValueForField = (formId, field) => useFormStore(formId, (state) => getPath(state.currentDefaultValues, field)); export const useFieldDefaultValue = (name, context) => { const defaultValues = useDefaultValuesForForm(context); const state = useCurrentDefaultValueForField(context.formId, name); return defaultValues.map((val) => getPath(val, name)).hydrateTo(state); }; export const useInternalIsSubmitting = (formId) => useFormStore(formId, (state) => state.isSubmitting); export const useInternalIsValid = (formId) => useFormStore(formId, (state) => state.isValid()); export const useInternalHasBeenSubmitted = (formId) => useFormStore(formId, (state) => state.hasBeenSubmitted); export const useValidateField = (formId) => useFormStore(formId, (state) => state.validateField); export const useValidate = (formId) => useFormStore(formId, (state) => state.validate); const noOpReceiver = () => () => { }; export const useRegisterReceiveFocus = (formId) => useFormStore(formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.registerReceiveFocus) !== null && _b !== void 0 ? _b : noOpReceiver; }); const defaultDefaultValues = {}; export const useSyncedDefaultValues = (formId) => useFormStore(formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.defaultValues) !== null && _b !== void 0 ? _b : defaultDefaultValues; }); export const useSetTouched = ({ formId }) => useFormStore(formId, (state) => state.setTouched); export const useTouchedFields = (formId) => useFormStore(formId, (state) => state.touchedFields); export const useFieldErrors = (formId) => useFormStore(formId, (state) => state.fieldErrors); export const useSetFieldErrors = (formId) => useFormStore(formId, (state) => state.setFieldErrors); export const useResetFormElement = (formId) => useFormStore(formId, (state) => state.resetFormElement); export const useSubmitForm = (formId) => useFormStore(formId, (state) => state.submit); export const useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.action; }); export const useFormSubactionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) === null || _a === void 0 ? void 0 : _a.subaction; }); export const useFormValues = (formId) => useFormStore(formId, (state) => state.getValues);