remix-validated-form
Version:
Form component and utils for easy form validation in remix
1,598 lines (1,569 loc) • 64.6 kB
JavaScript
"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,