remix-validated-form
Version:
Form component and utils for easy form validation in remix
119 lines (118 loc) • 6.97 kB
JavaScript
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);