UNPKG

remix-validated-form

Version:

Form component and utils for easy form validation in remix

1,598 lines (1,569 loc) 64.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from2, except, desc) => { if (from2 && typeof from2 === "object" || typeof from2 === "function") { for (let key of __getOwnPropNames(from2)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from2[key], enumerable: !(desc = __getOwnPropDesc(from2, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { FieldArray: () => FieldArray, ValidatedForm: () => ValidatedForm, createValidator: () => createValidator, setFormDefaults: () => setFormDefaults, useControlField: () => useControlField, useField: () => useField, useFieldArray: () => useFieldArray, useFormContext: () => useFormContext, useIsSubmitting: () => useIsSubmitting, useIsValid: () => useIsValid, useUpdateControlledField: () => useUpdateControlledField, validationError: () => validationError }); module.exports = __toCommonJS(src_exports); // src/hooks.ts var import_react5 = require("react"); // src/internal/getInputProps.ts var R = __toESM(require("remeda")); // src/internal/logic/getCheckboxChecked.ts var getCheckboxChecked = (checkboxValue = "on", newValue) => { if (Array.isArray(newValue)) return newValue.some((val) => val === true || val === checkboxValue); if (typeof newValue === "boolean") return newValue; if (typeof newValue === "string") return newValue === checkboxValue; return void 0; }; // src/internal/logic/getRadioChecked.ts var getRadioChecked = (radioValue = "on", newValue) => { if (typeof newValue === "string") return newValue === radioValue; return void 0; }; if (void 0) { const { it, expect } = void 0; it("getRadioChecked", () => { expect(getRadioChecked("on", "on")).toBe(true); expect(getRadioChecked("on", void 0)).toBe(void 0); expect(getRadioChecked("trueValue", void 0)).toBe(void 0); expect(getRadioChecked("trueValue", "bob")).toBe(false); expect(getRadioChecked("trueValue", "trueValue")).toBe(true); }); } // src/internal/getInputProps.ts var defaultValidationBehavior = { initial: "onBlur", whenTouched: "onChange", whenSubmitted: "onChange" }; var createGetInputProps = ({ clearError, validate, defaultValue, touched, setTouched, hasBeenSubmitted, validationBehavior, name }) => { const validationBehaviors = { ...defaultValidationBehavior, ...validationBehavior }; return (props = {}) => { const behavior = hasBeenSubmitted ? validationBehaviors.whenSubmitted : touched ? validationBehaviors.whenTouched : validationBehaviors.initial; const inputProps = { ...props, onChange: (...args) => { var _a; if (behavior === "onChange") validate(); else clearError(); return (_a = props == null ? void 0 : props.onChange) == null ? void 0 : _a.call(props, ...args); }, onBlur: (...args) => { var _a; if (behavior === "onBlur") validate(); setTouched(true); return (_a = props == null ? void 0 : props.onBlur) == null ? void 0 : _a.call(props, ...args); }, name }; if (props.type === "checkbox") { inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue); } else if (props.type === "radio") { inputProps.defaultChecked = getRadioChecked(props.value, defaultValue); } else if (props.value === void 0) { inputProps.defaultValue = defaultValue; } return R.omitBy(inputProps, (value) => value === void 0); }; }; // src/internal/hooks.ts var import_react2 = require("@remix-run/react"); var import_react3 = require("react"); // ../set-get/src/stringToPathArray.ts var stringToPathArray = (path) => { if (path.length === 0) return []; const match = path.match(/^\[(.+?)\](.*)$/) || path.match(/^\.?([^\.\[\]]+)(.*)$/); if (match) { const [_, key, rest] = match; return [/^\d+$/.test(key) ? Number(key) : key, ...stringToPathArray(rest)]; } return [path]; }; // ../set-get/src/setPath.ts function setPath(object, path, defaultValue) { return _setPathNormalized(object, stringToPathArray(path), defaultValue); } function _setPathNormalized(object, path, value) { var _a; const leadingSegments = path.slice(0, -1); const lastSegment = path[path.length - 1]; let obj = object; for (let i = 0; i < leadingSegments.length; i++) { const segment = leadingSegments[i]; if (obj[segment] === void 0) { const nextSegment = (_a = leadingSegments[i + 1]) != null ? _a : lastSegment; obj[segment] = typeof nextSegment === "number" ? [] : {}; } obj = obj[segment]; } obj[lastSegment] = value; return object; } // ../set-get/src/getPath.ts var import_lodash = __toESM(require("lodash.get")); var getPath = (object, path) => { return (0, import_lodash.default)(object, path); }; // src/internal/hooks.ts var import_tiny_invariant3 = __toESM(require("tiny-invariant")); // src/internal/constants.ts var FORM_ID_FIELD = "__rvfInternalFormId"; var FORM_DEFAULTS_FIELD = "__rvfInternalFormDefaults"; var formDefaultValuesKey = (formId) => `${FORM_DEFAULTS_FIELD}_${formId}`; // src/internal/formContext.ts var import_react = require("react"); var InternalFormContext = (0, import_react.createContext)(null); // src/internal/hydratable.ts var serverData = (data) => ({ hydrateTo: () => data, map: (fn) => serverData(fn(data)) }); var hydratedData = () => ({ hydrateTo: (hydratedData2) => hydratedData2, map: () => hydratedData() }); var from = (data, hydrated) => hydrated ? hydratedData() : serverData(data); var hydratable = { serverData, hydratedData, from }; // src/internal/state/createFormStore.ts var import_tiny_invariant2 = __toESM(require("tiny-invariant")); var import_zustand = require("zustand"); var import_immer = require("zustand/middleware/immer"); // src/internal/logic/requestSubmit.ts var requestSubmit = (element, submitter) => { if (typeof Object.getPrototypeOf(element).requestSubmit === "function" && true) { element.requestSubmit(submitter); return; } if (submitter) { validateSubmitter(element, submitter); submitter.click(); return; } const dummySubmitter = document.createElement("input"); dummySubmitter.type = "submit"; dummySubmitter.hidden = true; element.appendChild(dummySubmitter); dummySubmitter.click(); element.removeChild(dummySubmitter); }; function validateSubmitter(element, submitter) { const isHtmlElement = submitter instanceof HTMLElement; if (!isHtmlElement) { raise(TypeError, "parameter 1 is not of type 'HTMLElement'"); } const hasSubmitType = "type" in submitter && submitter.type === "submit"; if (!hasSubmitType) raise(TypeError, "The specified element is not a submit button"); const isForCorrectForm = "form" in submitter && submitter.form === element; if (!isForCorrectForm) raise( DOMException, "The specified element is not owned by this form element", "NotFoundError" ); } function raise(errorConstructor, message, name) { throw new errorConstructor( "Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name ); } if (void 0) { const { it, expect } = void 0; it("should validate the submitter", () => { const form = document.createElement("form"); document.body.appendChild(form); const submitter = document.createElement("input"); expect(() => validateSubmitter(null, null)).toThrow(); expect(() => validateSubmitter(form, null)).toThrow(); expect(() => validateSubmitter(form, submitter)).toThrow(); expect( () => validateSubmitter(form, document.createElement("div")) ).toThrow(); submitter.type = "submit"; expect(() => validateSubmitter(form, submitter)).toThrow(); form.appendChild(submitter); expect(() => validateSubmitter(form, submitter)).not.toThrow(); form.removeChild(submitter); expect(() => validateSubmitter(form, submitter)).toThrow(); document.body.appendChild(submitter); form.id = "test-form"; submitter.setAttribute("form", "test-form"); expect(() => validateSubmitter(form, submitter)).not.toThrow(); const button = document.createElement("button"); button.type = "submit"; form.appendChild(button); expect(() => validateSubmitter(form, button)).not.toThrow(); }); } // src/internal/state/arrayUtil.ts var import_tiny_invariant = __toESM(require("tiny-invariant")); var getArray = (values, field) => { const value = getPath(values, field); if (value === void 0 || value === null) { const newValue = []; setPath(values, field, newValue); return newValue; } (0, import_tiny_invariant.default)( Array.isArray(value), `FieldArray: defaultValue value for ${field} must be an array, null, or undefined` ); return value; }; var swap = (array, indexA, indexB) => { const itemA = array[indexA]; const itemB = array[indexB]; const hasItemA = indexA in array; const hasItemB = indexB in array; if (hasItemA) { array[indexB] = itemA; } else { delete array[indexB]; } if (hasItemB) { array[indexA] = itemB; } else { delete array[indexA]; } }; function sparseSplice(array, start, deleteCount, item) { if (array.length < start && item) { array.length = start; } if (arguments.length === 4) return array.splice(start, deleteCount, item); else if (arguments.length === 3) return array.splice(start, deleteCount); return array.splice(start); } var move = (array, from2, to) => { const [item] = sparseSplice(array, from2, 1); sparseSplice(array, to, 0, item); }; var insert = (array, index, value) => { sparseSplice(array, index, 0, value); }; var insertEmpty = (array, index) => { const tail = sparseSplice(array, index); tail.forEach((item, i) => { sparseSplice(array, index + i + 1, 0, item); }); }; var remove = (array, index) => { sparseSplice(array, index, 1); }; var replace = (array, index, value) => { sparseSplice(array, index, 1, value); }; var mutateAsArray = (field, obj, mutate) => { const beforeKeys = /* @__PURE__ */ new Set(); const arr = []; for (const [key, value] of Object.entries(obj)) { if (key.startsWith(field) && key !== field) { beforeKeys.add(key); setPath(arr, key.substring(field.length), value); } } mutate(arr); for (const key of beforeKeys) { delete obj[key]; } const newKeys = getDeepArrayPaths(arr); for (const key of newKeys) { const val = getPath(arr, key); if (val !== void 0) { obj[`${field}${key}`] = val; } } }; var getDeepArrayPaths = (obj, basePath = "") => { if (Array.isArray(obj)) { return obj.flatMap( (item, index) => getDeepArrayPaths(item, `${basePath}[${index}]`) ); } if (typeof obj === "object") { return Object.keys(obj).flatMap( (key) => getDeepArrayPaths(obj[key], `${basePath}.${key}`) ); } return [basePath]; }; if (void 0) { const { describe, expect, it } = void 0; const countArrayItems = (arr) => { let count = 0; arr.forEach(() => count++); return count; }; describe("getArray", () => { it("shoud get a deeply nested array that can be mutated to update the nested value", () => { const values = { d: [ { foo: "bar", baz: [true, false] }, { e: true, f: "hi" } ] }; const result = getArray(values, "d[0].baz"); const finalValues = { d: [ { foo: "bar", baz: [true, false, true] }, { e: true, f: "hi" } ] }; expect(result).toEqual([true, false]); result.push(true); expect(values).toEqual(finalValues); }); it("should return an empty array that can be mutated if result is null or undefined", () => { const values = {}; const result = getArray(values, "a.foo[0].bar"); const finalValues = { a: { foo: [{ bar: ["Bob ross"] }] } }; expect(result).toEqual([]); result.push("Bob ross"); expect(values).toEqual(finalValues); }); it("should throw if the value is defined and not an array", () => { const values = { foo: "foo" }; expect(() => getArray(values, "foo")).toThrow(); }); }); describe("swap", () => { it("should swap two items", () => { const array = [1, 2, 3]; swap(array, 0, 1); expect(array).toEqual([2, 1, 3]); }); it("should work for sparse arrays", () => { const arr = []; arr[0] = true; swap(arr, 0, 2); expect(countArrayItems(arr)).toEqual(1); expect(0 in arr).toBe(false); expect(2 in arr).toBe(true); expect(arr[2]).toEqual(true); }); }); describe("move", () => { it("should move an item to a new index", () => { const array = [1, 2, 3]; move(array, 0, 1); expect(array).toEqual([2, 1, 3]); }); it("should work with sparse arrays", () => { const array = [1]; move(array, 0, 2); expect(countArrayItems(array)).toEqual(1); expect(array).toEqual([void 0, void 0, 1]); }); }); describe("insert", () => { it("should insert an item at a new index", () => { const array = [1, 2, 3]; insert(array, 1, 4); expect(array).toEqual([1, 4, 2, 3]); }); it("should be able to insert falsey values", () => { const array = [1, 2, 3]; insert(array, 1, null); expect(array).toEqual([1, null, 2, 3]); }); it("should handle sparse arrays", () => { const array = []; array[2] = true; insert(array, 0, true); expect(countArrayItems(array)).toEqual(2); expect(array).toEqual([true, void 0, void 0, true]); }); }); describe("insertEmpty", () => { it("should insert an empty item at a given index", () => { const array = [1, 2, 3]; insertEmpty(array, 1); expect(array).toStrictEqual([1, , 2, 3]); expect(array).not.toStrictEqual([1, void 0, 2, 3]); }); it("should work with already sparse arrays", () => { const array = [, , 1, , 2, , 3]; insertEmpty(array, 3); expect(array).toStrictEqual([, , 1, , , 2, , 3]); expect(array).not.toStrictEqual([ void 0, void 0, 1, void 0, void 0, 2, void 0, 3 ]); }); }); describe("remove", () => { it("should remove an item at a given index", () => { const array = [1, 2, 3]; remove(array, 1); expect(array).toEqual([1, 3]); }); it("should handle sparse arrays", () => { const array = []; array[2] = true; remove(array, 0); expect(countArrayItems(array)).toEqual(1); expect(array).toEqual([void 0, true]); }); }); describe("replace", () => { it("should replace an item at a given index", () => { const array = [1, 2, 3]; replace(array, 1, 4); expect(array).toEqual([1, 4, 3]); }); it("should handle sparse arrays", () => { const array = []; array[2] = true; replace(array, 0, true); expect(countArrayItems(array)).toEqual(2); expect(array).toEqual([true, void 0, true]); }); }); describe("mutateAsArray", () => { it("should handle swap", () => { const values = { myField: "something", "myField[0]": "foo", "myField[2]": "bar", otherField: "baz", "otherField[0]": "something else" }; mutateAsArray("myField", values, (arr) => { swap(arr, 0, 2); }); expect(values).toEqual({ myField: "something", "myField[0]": "bar", "myField[2]": "foo", otherField: "baz", "otherField[0]": "something else" }); }); it("should swap sparse arrays", () => { const values = { myField: "something", "myField[0]": "foo", otherField: "baz", "otherField[0]": "something else" }; mutateAsArray("myField", values, (arr) => { swap(arr, 0, 2); }); expect(values).toEqual({ myField: "something", "myField[2]": "foo", otherField: "baz", "otherField[0]": "something else" }); }); it("should handle arrays with nested values", () => { const values = { myField: "something", "myField[0].title": "foo", "myField[0].note": "bar", "myField[2].title": "other", "myField[2].note": "other", otherField: "baz", "otherField[0]": "something else" }; mutateAsArray("myField", values, (arr) => { swap(arr, 0, 2); }); expect(values).toEqual({ myField: "something", "myField[0].title": "other", "myField[0].note": "other", "myField[2].title": "foo", "myField[2].note": "bar", otherField: "baz", "otherField[0]": "something else" }); }); it("should handle move", () => { const values = { myField: "something", "myField[0]": "foo", "myField[1]": "bar", "myField[2]": "baz", "otherField[0]": "something else" }; mutateAsArray("myField", values, (arr) => { move(arr, 0, 2); }); expect(values).toEqual({ myField: "something", "myField[0]": "bar", "myField[1]": "baz", "myField[2]": "foo", "otherField[0]": "something else" }); }); it("should not create keys for `undefined`", () => { const values = { "myField[0]": "foo" }; mutateAsArray("myField", values, (arr) => { arr.unshift(void 0); }); expect(Object.keys(values)).toHaveLength(1); expect(values).toEqual({ "myField[1]": "foo" }); }); it("should handle remove", () => { const values = { myField: "something", "myField[0]": "foo", "myField[1]": "bar", "myField[2]": "baz", "otherField[0]": "something else" }; mutateAsArray("myField", values, (arr) => { remove(arr, 1); }); expect(values).toEqual({ myField: "something", "myField[0]": "foo", "myField[1]": "baz", "otherField[0]": "something else" }); expect("myField[2]" in values).toBe(false); }); }); describe("getDeepArrayPaths", () => { it("should return all paths recursively", () => { const obj = [ true, true, [true, true], { foo: true, bar: { baz: true, test: [true] } } ]; expect(getDeepArrayPaths(obj, "myField")).toEqual([ "myField[0]", "myField[1]", "myField[2][0]", "myField[2][1]", "myField[3].foo", "myField[3].bar.baz", "myField[3].bar.test[0]" ]); }); }); } // src/internal/state/createFormStore.ts var noOp = () => { }; var defaultFormState = { isHydrated: false, isSubmitting: false, hasBeenSubmitted: false, touchedFields: {}, fieldErrors: {}, formElement: null, isValid: () => true, startSubmit: noOp, endSubmit: noOp, setTouched: noOp, setFieldError: noOp, setFieldErrors: noOp, clearFieldError: noOp, currentDefaultValues: {}, reset: () => noOp, syncFormProps: noOp, setFormElement: noOp, validate: async () => { throw new Error("Validate called before form was initialized."); }, smartValidate: async () => { throw new Error("Validate called before form was initialized."); }, submit: async () => { throw new Error("Submit called before form was initialized."); }, resetFormElement: noOp, getValues: () => new FormData(), controlledFields: { values: {}, refCounts: {}, valueUpdatePromises: {}, valueUpdateResolvers: {}, register: noOp, unregister: noOp, setValue: noOp, getValue: noOp, kickoffValueUpdate: noOp, awaitValueUpdate: async () => { throw new Error("AwaitValueUpdate called before form was initialized."); }, array: { push: noOp, swap: noOp, move: noOp, insert: noOp, unshift: noOp, remove: noOp, pop: noOp, replace: noOp } } }; var createFormState = (set, get2) => ({ isHydrated: false, isSubmitting: false, hasBeenSubmitted: false, touchedFields: {}, fieldErrors: {}, formElement: null, currentDefaultValues: {}, isValid: () => Object.keys(get2().fieldErrors).length === 0, startSubmit: () => set((state) => { state.isSubmitting = true; state.hasBeenSubmitted = true; }), endSubmit: () => set((state) => { state.isSubmitting = false; }), setTouched: (fieldName, touched) => set((state) => { state.touchedFields[fieldName] = touched; }), setFieldError: (fieldName, error) => set((state) => { state.fieldErrors[fieldName] = error; }), setFieldErrors: (errors) => set((state) => { state.fieldErrors = errors; }), clearFieldError: (fieldName) => set((state) => { delete state.fieldErrors[fieldName]; }), reset: () => set((state) => { var _a, _b; state.fieldErrors = {}; state.touchedFields = {}; state.hasBeenSubmitted = false; const nextDefaults = (_b = (_a = state.formProps) == null ? void 0 : _a.defaultValues) != null ? _b : {}; state.controlledFields.values = nextDefaults; state.currentDefaultValues = nextDefaults; }), syncFormProps: (props) => set((state) => { if (!state.isHydrated) { state.controlledFields.values = props.defaultValues; state.currentDefaultValues = props.defaultValues; } state.formProps = props; state.isHydrated = true; }), setFormElement: (formElement) => { if (get2().formElement === formElement) return; set((state) => { state.formElement = formElement; }); }, validate: async () => { var _a; const formElement = get2().formElement; (0, import_tiny_invariant2.default)( formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form." ); const validator = (_a = get2().formProps) == null ? void 0 : _a.validator; (0, import_tiny_invariant2.default)( validator, "Cannot find validator. This is probably a bug in remix-validated-form." ); const result = await validator.validate(new FormData(formElement)); if (result.error) get2().setFieldErrors(result.error.fieldErrors); return result; }, smartValidate: async ({ alwaysIncludeErrorsFromFields = [] } = {}) => { var _a; const formElement = get2().formElement; (0, import_tiny_invariant2.default)( formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form." ); const validator = (_a = get2().formProps) == null ? void 0 : _a.validator; (0, import_tiny_invariant2.default)( validator, "Cannot find validator. This is probably a bug in remix-validated-form." ); await Promise.all( alwaysIncludeErrorsFromFields.map( (field) => { var _a2, _b; return (_b = (_a2 = get2().controlledFields).awaitValueUpdate) == null ? void 0 : _b.call(_a2, field); } ) ); const validationResult = await validator.validate( new FormData(formElement) ); if (!validationResult.error) { const hadErrors = Object.keys(get2().fieldErrors).length > 0; if (hadErrors) get2().setFieldErrors({}); return validationResult; } const { error: { fieldErrors } } = validationResult; const errorFields = /* @__PURE__ */ new Set(); const incomingErrors = /* @__PURE__ */ new Set(); const prevErrors = /* @__PURE__ */ new Set(); Object.keys(fieldErrors).forEach((field) => { errorFields.add(field); incomingErrors.add(field); }); Object.keys(get2().fieldErrors).forEach((field) => { errorFields.add(field); prevErrors.add(field); }); const fieldsToUpdate = /* @__PURE__ */ new Set(); const fieldsToDelete = /* @__PURE__ */ new Set(); errorFields.forEach((field) => { if (!incomingErrors.has(field)) { fieldsToDelete.add(field); return; } if (prevErrors.has(field) && incomingErrors.has(field)) { if (fieldErrors[field] !== get2().fieldErrors[field]) fieldsToUpdate.add(field); return; } if (alwaysIncludeErrorsFromFields.includes(field)) { fieldsToUpdate.add(field); return; } if (!prevErrors.has(field)) { const fieldTouched = get2().touchedFields[field]; const formHasBeenSubmitted = get2().hasBeenSubmitted; if (fieldTouched || formHasBeenSubmitted) fieldsToUpdate.add(field); return; } }); if (fieldsToDelete.size === 0 && fieldsToUpdate.size === 0) { return { ...validationResult, error: { fieldErrors: get2().fieldErrors } }; } set((state) => { fieldsToDelete.forEach((field) => { delete state.fieldErrors[field]; }); fieldsToUpdate.forEach((field) => { state.fieldErrors[field] = fieldErrors[field]; }); }); return { ...validationResult, error: { fieldErrors: get2().fieldErrors } }; }, submit: () => { const formElement = get2().formElement; (0, import_tiny_invariant2.default)( formElement, "Cannot find reference to form. This is probably a bug in remix-validated-form." ); requestSubmit(formElement); }, getValues: () => { var _a; return new FormData((_a = get2().formElement) != null ? _a : void 0); }, resetFormElement: () => { var _a; return (_a = get2().formElement) == null ? void 0 : _a.reset(); }, controlledFields: { values: {}, refCounts: {}, valueUpdatePromises: {}, valueUpdateResolvers: {}, register: (fieldName) => { set((state) => { var _a; const current = (_a = state.controlledFields.refCounts[fieldName]) != null ? _a : 0; state.controlledFields.refCounts[fieldName] = current + 1; }); }, unregister: (fieldName) => { if (get2() === null || get2() === void 0) return; set((state) => { var _a, _b, _c; const current = (_a = state.controlledFields.refCounts[fieldName]) != null ? _a : 0; if (current > 1) { state.controlledFields.refCounts[fieldName] = current - 1; return; } const isNested = Object.keys(state.controlledFields.refCounts).some( (key) => fieldName.startsWith(key) && key !== fieldName ); if (!isNested) { setPath( state.controlledFields.values, fieldName, getPath((_b = state.formProps) == null ? void 0 : _b.defaultValues, fieldName) ); setPath( state.currentDefaultValues, fieldName, getPath((_c = state.formProps) == null ? void 0 : _c.defaultValues, fieldName) ); } delete state.controlledFields.refCounts[fieldName]; }); }, getValue: (fieldName) => getPath(get2().controlledFields.values, fieldName), setValue: (fieldName, value) => { set((state) => { setPath(state.controlledFields.values, fieldName, value); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, kickoffValueUpdate: (fieldName) => { const clear = () => set((state) => { delete state.controlledFields.valueUpdateResolvers[fieldName]; delete state.controlledFields.valueUpdatePromises[fieldName]; }); set((state) => { const promise = new Promise((resolve) => { state.controlledFields.valueUpdateResolvers[fieldName] = resolve; }).then(clear); state.controlledFields.valueUpdatePromises[fieldName] = promise; }); }, awaitValueUpdate: async (fieldName) => { await get2().controlledFields.valueUpdatePromises[fieldName]; }, array: { push: (fieldName, item) => { set((state) => { getArray(state.controlledFields.values, fieldName).push(item); getArray(state.currentDefaultValues, fieldName).push(item); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, swap: (fieldName, indexA, indexB) => { set((state) => { swap( getArray(state.controlledFields.values, fieldName), indexA, indexB ); swap( getArray(state.currentDefaultValues, fieldName), indexA, indexB ); mutateAsArray( fieldName, state.touchedFields, (array) => swap(array, indexA, indexB) ); mutateAsArray( fieldName, state.fieldErrors, (array) => swap(array, indexA, indexB) ); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, move: (fieldName, from2, to) => { set((state) => { move( getArray(state.controlledFields.values, fieldName), from2, to ); move( getArray(state.currentDefaultValues, fieldName), from2, to ); mutateAsArray( fieldName, state.touchedFields, (array) => move(array, from2, to) ); mutateAsArray( fieldName, state.fieldErrors, (array) => move(array, from2, to) ); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, insert: (fieldName, index, item) => { set((state) => { insert( getArray(state.controlledFields.values, fieldName), index, item ); insert( getArray(state.currentDefaultValues, fieldName), index, item ); mutateAsArray( fieldName, state.touchedFields, (array) => insertEmpty(array, index) ); mutateAsArray( fieldName, state.fieldErrors, (array) => insertEmpty(array, index) ); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, remove: (fieldName, index) => { set((state) => { remove( getArray(state.controlledFields.values, fieldName), index ); remove( getArray(state.currentDefaultValues, fieldName), index ); mutateAsArray( fieldName, state.touchedFields, (array) => remove(array, index) ); mutateAsArray( fieldName, state.fieldErrors, (array) => remove(array, index) ); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, pop: (fieldName) => { set((state) => { getArray(state.controlledFields.values, fieldName).pop(); getArray(state.currentDefaultValues, fieldName).pop(); mutateAsArray( fieldName, state.touchedFields, (array) => array.pop() ); mutateAsArray( fieldName, state.fieldErrors, (array) => array.pop() ); }); get2().controlledFields.kickoffValueUpdate(fieldName); }, unshift: (fieldName, value) => { set((state) => { getArray(state.controlledFields.values, fieldName).unshift(value); getArray(state.currentDefaultValues, fieldName).unshift(value); mutateAsArray( fieldName, state.touchedFields, (array) => insertEmpty(array, 0) ); mutateAsArray( fieldName, state.fieldErrors, (array) => insertEmpty(array, 0) ); }); }, replace: (fieldName, index, item) => { set((state) => { replace( getArray(state.controlledFields.values, fieldName), index, item ); replace( getArray(state.currentDefaultValues, fieldName), index, item ); mutateAsArray( fieldName, state.touchedFields, (array) => replace(array, index, item) ); mutateAsArray( fieldName, state.fieldErrors, (array) => replace(array, index, item) ); }); get2().controlledFields.kickoffValueUpdate(fieldName); } } } }); var useRootFormStore = (0, import_zustand.create)()( (0, import_immer.immer)((set, get2) => ({ forms: {}, form: (formId) => { var _a; return (_a = get2().forms[formId]) != null ? _a : defaultFormState; }, cleanupForm: (formId) => { set((state) => { delete state.forms[formId]; }); }, registerForm: (formId) => { if (get2().forms[formId]) return; set((state) => { state.forms[formId] = createFormState( (setter) => set((state2) => setter(state2.forms[formId])), () => get2().forms[formId] ); }); } })) ); // src/internal/state/storeHooks.ts var useFormStore = (formId, selector) => { return useRootFormStore((state) => selector(state.form(formId))); }; // src/internal/hooks.ts var useInternalFormContext = (formId, hookName) => { const formContext = (0, import_react3.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'.` ); }; function useErrorResponseForForm({ fetcher, subaction, formId }) { var _a; const actionData = (0, import_react2.useActionData)(); if (fetcher) { if ((_a = fetcher.data) == null ? void 0 : _a.fieldErrors) return fetcher.data; return null; } if (!(actionData == null ? void 0 : actionData.fieldErrors)) return null; if (typeof formId === "string" && actionData.formId) return actionData.formId === formId ? actionData : null; if (!subaction && !actionData.subaction || actionData.subaction === subaction) return actionData; return null; } var useFieldErrorsForForm = (context) => { const response = useErrorResponseForForm(context); const hydrated = useFormStore(context.formId, (state) => state.isHydrated); return hydratable.from(response == null ? void 0 : response.fieldErrors, hydrated); }; var useDefaultValuesFromLoader = ({ formId }) => { const matches = (0, import_react2.useMatches)(); if (typeof formId === "string") { const dataKey = formDefaultValuesKey(formId); const match = matches.reverse().find( (match2) => match2.data && typeof match2.data === "object" && dataKey in match2.data ); return match == null ? void 0 : match.data[dataKey]; } return null; }; var useDefaultValuesForForm = (context) => { const { formId, defaultValuesProp } = context; const hydrated = useFormStore(formId, (state) => state.isHydrated); const errorResponse = useErrorResponseForForm(context); const defaultValuesFromLoader = useDefaultValuesFromLoader(context); if (hydrated) return hydratable.hydratedData(); if (errorResponse == null ? void 0 : errorResponse.repopulateFields) { (0, import_tiny_invariant3.default)( 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); }; var useHasActiveFormSubmit = ({ fetcher }) => { let navigation = (0, import_react2.useNavigation)(); const hasActiveSubmission = fetcher ? fetcher.state === "submitting" : navigation.state === "submitting" || navigation.state === "loading"; return hasActiveSubmission; }; var useFieldTouched = (field, { formId }) => { const touched = useFormStore(formId, (state) => state.touchedFields[field]); const setFieldTouched = useFormStore(formId, (state) => state.setTouched); const setTouched = (0, import_react3.useCallback)( (touched2) => setFieldTouched(field, touched2), [field, setFieldTouched] ); return [touched, setTouched]; }; var useFieldError = (name, context) => { const fieldErrors = useFieldErrorsForForm(context); const state = useFormStore( context.formId, (state2) => state2.fieldErrors[name] ); return fieldErrors.map((fieldErrors2) => fieldErrors2 == null ? void 0 : fieldErrors2[name]).hydrateTo(state); }; var useClearError = (context) => { const { formId } = context; return useFormStore(formId, (state) => state.clearFieldError); }; var useCurrentDefaultValueForField = (formId, field) => useFormStore(formId, (state) => getPath(state.currentDefaultValues, field)); var useFieldDefaultValue = (name, context) => { const defaultValues = useDefaultValuesForForm(context); const state = useCurrentDefaultValueForField(context.formId, name); return defaultValues.map((val) => getPath(val, name)).hydrateTo(state); }; var useInternalIsSubmitting = (formId) => useFormStore(formId, (state) => state.isSubmitting); var useInternalIsValid = (formId) => useFormStore(formId, (state) => state.isValid()); var useInternalHasBeenSubmitted = (formId) => useFormStore(formId, (state) => state.hasBeenSubmitted); var useSmartValidate = (formId) => useFormStore(formId, (state) => state.smartValidate); var useValidate = (formId) => useFormStore(formId, (state) => state.validate); var noOpReceiver = () => () => { }; var useRegisterReceiveFocus = (formId) => useFormStore( formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) == null ? void 0 : _a.registerReceiveFocus) != null ? _b : noOpReceiver; } ); var defaultDefaultValues = {}; var useSyncedDefaultValues = (formId) => useFormStore( formId, (state) => { var _a, _b; return (_b = (_a = state.formProps) == null ? void 0 : _a.defaultValues) != null ? _b : defaultDefaultValues; } ); var useSetTouched = ({ formId }) => useFormStore(formId, (state) => state.setTouched); var useTouchedFields = (formId) => useFormStore(formId, (state) => state.touchedFields); var useFieldErrors = (formId) => useFormStore(formId, (state) => state.fieldErrors); var useSetFieldErrors = (formId) => useFormStore(formId, (state) => state.setFieldErrors); var useResetFormElement = (formId) => useFormStore(formId, (state) => state.resetFormElement); var useSubmitForm = (formId) => useFormStore(formId, (state) => state.submit); var useFormActionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) == null ? void 0 : _a.action; }); var useFormSubactionProp = (formId) => useFormStore(formId, (state) => { var _a; return (_a = state.formProps) == null ? void 0 : _a.subaction; }); var useFormValues = (formId) => useFormStore(formId, (state) => state.getValues); // src/internal/state/controlledFields.ts var import_react4 = require("react"); var useControlledFieldValue = (context, field) => { const value = useFormStore( context.formId, (state) => state.controlledFields.getValue(field) ); const isFormHydrated = useFormStore( context.formId, (state) => state.isHydrated ); const defaultValue = useFieldDefaultValue(field, context); return isFormHydrated ? value : defaultValue; }; var useRegisterControlledField = (context, field) => { const resolveUpdate = useFormStore( context.formId, (state) => state.controlledFields.valueUpdateResolvers[field] ); (0, import_react4.useEffect)(() => { resolveUpdate == null ? void 0 : resolveUpdate(); }, [resolveUpdate]); const register = useFormStore( context.formId, (state) => state.controlledFields.register ); const unregister = useFormStore( context.formId, (state) => state.controlledFields.unregister ); (0, import_react4.useEffect)(() => { register(field); return () => unregister(field); }, [context.formId, field, register, unregister]); }; var useControllableValue = (context, field) => { useRegisterControlledField(context, field); const setControlledFieldValue = useFormStore( context.formId, (state) => state.controlledFields.setValue ); const setValue = (0, import_react4.useCallback)( (value2) => setControlledFieldValue(field, value2), [field, setControlledFieldValue] ); const value = useControlledFieldValue(context, field); return [value, setValue]; }; var useUpdateControllableValue = (formId) => { const setValue = useFormStore( formId, (state) => state.controlledFields.setValue ); return (0, import_react4.useCallback)( (field, value) => setValue(field, value), [setValue] ); }; // src/hooks.ts var useIsSubmitting = (formId) => { const formContext = useInternalFormContext(formId, "useIsSubmitting"); return useInternalIsSubmitting(formContext.formId); }; var useIsValid = (formId) => { const formContext = useInternalFormContext(formId, "useIsValid"); return useInternalIsValid(formContext.formId); }; var useField = (name, options) => { const { formId: providedFormId, handleReceiveFocus } = options != null ? options : {}; const formContext = useInternalFormContext(providedFormId, "useField"); const defaultValue = useFieldDefaultValue(name, formContext); const [touched, setTouched] = useFieldTouched(name, formContext); const error = useFieldError(name, formContext); const clearError = useClearError(formContext); const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId); const smartValidate = useSmartValidate(formContext.formId); const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId); (0, import_react5.useEffect)(() => { if (handleReceiveFocus) return registerReceiveFocus(name, handleReceiveFocus); }, [handleReceiveFocus, name, registerReceiveFocus]); const field = (0, import_react5.useMemo)(() => { const helpers = { error, clearError: () => clearError(name), validate: () => smartValidate({ alwaysIncludeErrorsFromFields: [name] }), defaultValue, touched, setTouched }; const getInputProps = createGetInputProps({ ...helpers, name, hasBeenSubmitted, validationBehavior: options == null ? void 0 : options.validationBehavior }); return { ...helpers, getInputProps }; }, [ error, clearError, defaultValue, touched, setTouched, name, hasBeenSubmitted, options == null ? void 0 : options.validationBehavior, smartValidate ]); return field; }; var useControlField = (name, formId) => { const context = useInternalFormContext(formId, "useControlField"); const [value, setValue] = useControllableValue(context, name); return [value, setValue]; }; var useUpdateControlledField = (formId) => { const context = useInternalFormContext(formId, "useControlField"); return useUpdateControllableValue(context.formId); }; // src/server.ts var import_server_runtime = require("@remix-run/server-runtime"); function validationError(error, repopulateFields, init) { return (0, import_server_runtime.json)( { fieldErrors: error.fieldErrors, subaction: error.subaction, repopulateFields, formId: error.formId }, { status: 422, ...init } ); } var setFormDefaults = (formId, defaultValues) => ({ [formDefaultValuesKey(formId)]: defaultValues }); // src/ValidatedForm.tsx var import_react9 = require("@remix-run/react"); var import_react10 = require("react"); var R3 = __toESM(require("remeda")); // src/internal/MultiValueMap.ts var import_react6 = require("react"); var MultiValueMap = class { constructor() { this.dict = /* @__PURE__ */ new Map(); this.add = (key, value) => { if (this.dict.has(key)) { this.dict.get(key).push(value); } else { this.dict.set(key, [value]); } }; this.delete = (key) => { this.dict.delete(key); }; this.remove = (key, value) => { if (!this.dict.has(key)) return; const array = this.dict.get(key); const index = array.indexOf(value); if (index !== -1) array.splice(index, 1); if (array.length === 0) this.dict.delete(key); }; this.getAll = (key) => { var _a; return (_a = this.dict.get(key)) != null ? _a : []; }; this.entries = () => this.dict.entries(); this.values = () => this.dict.values(); this.has = (key) => this.dict.has(key); } }; var useMultiValueMap = () => { const ref = (0, import_react6.useRef)(null); return (0, import_react6.useCallback)(() => { if (ref.current) return ref.current; ref.current = new MultiValueMap(); return ref.current; }, []); }; // src/internal/submissionCallbacks.ts var import_react7 = require("react"); function useSubmitComplete(isSubmitting, callback) { const isPending = (0, import_react7.useRef)(false); (0, import_react7.useEffect)(() => { if (isSubmitting) { isPending.current = true; } if (!isSubmitting && isPending.current) { isPending.current = false; callback(); } }); } // src/internal/util.ts var import_react8 = require("react"); var R2 = __toESM(require("remeda")); var mergeRefs = (refs) => { return (value) => { refs.filter(Boolean).forEach((ref) => { if (typeof ref === "function") { ref(value); } else if (ref != null) { ref.current = value; } }); }; }; var useIsomorphicLayoutEffect = typeof window !== "undefined" ? import_react8.useLayoutEffect : import_react8.useEffect; var useDeepEqualsMemo = (item) => { const ref = (0, import_react8.useRef)(item); const areEqual = ref.current === item || R2.equals(ref.current, item); (0, import_react8.useEffect)(() => { if (!areEqual) { ref.current = item; } }); return areEqual ? ref.current : item; }; // src/ValidatedForm.tsx var import_jsx_runtime = require("react/jsx-runtime"); var getDataFromForm = (el) => new FormData(el); function nonNull(value) { return value !== null; } var focusFirstInvalidInput = (fieldErrors, customFocusHandlers, formElement) => { var _a; const namesInOrder = [...formElement.elements].map((el) => { const input = el instanceof RadioNodeList ? el[0] : el; if (input instanceof HTMLElement && "name" in input) return input.name; return null; }).filter(nonNull).filter((name) => name in fieldErrors); const uniqueNamesInOrder = R3.uniq(namesInOrder); for (const fieldName of uniqueNamesInOrder) { if (customFocusHandlers.has(fieldName)) { customFocusHandlers.getAll(fieldName).forEach((handler) => { handler(); }); break; } const elem = formElement.elements.namedItem(fieldName); if (!elem) continue; if (elem instanceof RadioNodeList) { const selectedRadio = (_a = [...elem].filter( (item) => item instanceof HTMLInputElement ).find((item) => item.value === elem.value)) != null ? _a : elem[0]; if (selectedRadio && selectedRadio instanceof HTMLInputElement) { selectedRadio.focus(); break; } } if (elem instanceof HTMLElement) { if (elem instanceof HTMLInputElement && elem.type === "hidden") { continue; } elem.focus(); break; } } }; var useFormId = (providedId) => { const [symbolId] = (0, import_react10.useState)(() => Symbol("remix-validated-form-id")); return providedId != null ? providedId : symbolId; }; var FormResetter = ({ resetAfterSubmit, formRef }) => { const isSubmitting = useIsSubmitting(); const isValid = useIsValid(); useSubmitComplete(isSubmitting, () => { var _a; if (isValid && resetAfterSubmit) { (_a = formRef.current) == null ? void 0 : _a.reset(); } }); return null; }; function formEventProxy(event) { let defaultPrevented = false; return new Proxy(event, { get: (target, prop) => { if (prop === "preventDefault") { return () => { defaultPrevented = true; }; } if (prop === "defaultPrevented") { return defaultPrevented; } return target[prop]; } }); } function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace: replace2, id, preventScrollReset, relative, encType, ...rest }) { var _a; const formId = useFormId(id); const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults); const contextValue = (0, import_react10.useMemo)( () => ({ formId, action, subaction, defaultValuesProp: providedDefaultValues,