UNPKG

@modular-forms/react

Version:

The modular and type-safe form library for React

1,384 lines (1,383 loc) 45.2 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2; var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2); import { useMemo, useEffect } from "react"; import { signal, batch, computed, useSignal, useComputed, effect } from "@preact/signals-react"; import { jsx, Fragment } from "react/jsx-runtime"; import { safeParseAsync, getDotPath } from "valibot"; function valiField(schema) { return async (value2) => { const result = await safeParseAsync(schema, value2, { abortPipeEarly: true }); return result.issues?.[0]?.message || ""; }; } function valiForm(schema) { return async (values) => { const result = await safeParseAsync(schema, values, { abortPipeEarly: true }); const formErrors = {}; if (result.issues) { for (const issue of result.issues) { formErrors[getDotPath(issue)] = issue.message; } } return formErrors; }; } function zodField(schema) { return async (value2) => { const result = await schema.safeParseAsync(value2); return result.success ? "" : result.error.issues[0].message; }; } function zodForm(schema) { return async (values) => { const result = await schema.safeParseAsync(values); const formErrors = {}; if (!result.success) { for (const issue of result.error.issues) { const path = issue.path.join("."); if (!formErrors[path]) { formErrors[path] = issue.message; } } } return formErrors; }; } function useFormStore({ initialValues = {}, validateOn = "submit", revalidateOn = "change", validate: validate2 } = {}) { return useMemo( () => ({ internal: { // Props initialValues, validate: validate2, validateOn, revalidateOn, // Signals fieldNames: signal([]), fieldArrayNames: signal([]), // Stores fields: {}, fieldArrays: {}, // Other validators: /* @__PURE__ */ new Set() }, // Signals element: signal(null), submitCount: signal(0), submitting: signal(false), submitted: signal(false), validating: signal(false), touched: signal(false), dirty: signal(false), invalid: signal(false), response: signal({}) }), // eslint-disable-next-line react-hooks/exhaustive-deps [] ); } function useForm(options) { const form = useFormStore(options); return useMemo( () => [ form, { Form: (props) => Form({ ...props, of: form }), Field: (props) => Field({ ...props, of: form }), FieldArray: (props) => FieldArray({ ...props, of: form }) } ], [form] ); } function getElementInput(element, field, type) { const { checked, files, options, value: value2, valueAsDate, valueAsNumber } = element; return !type || type === "string" ? value2 : type === "string[]" ? options ? [...options].filter((e) => e.selected && !e.disabled).map((e) => e.value) : checked ? [...field.value.peek() || [], value2] : (field.value.peek() || []).filter((v) => v !== value2) : type === "number" ? valueAsNumber : type === "boolean" ? checked : type === "File" && files ? files[0] : type === "File[]" && files ? [...files] : type === "Date" && valueAsDate ? valueAsDate : field.value.peek(); } function getFieldAndArrayStores(form) { return [ ...Object.values(form.internal.fields), ...Object.values(form.internal.fieldArrays) ]; } function getFieldArrayStore(form, name) { return form.internal.fieldArrays[name]; } function getPathIndex(name, path) { return +path.replace(`${name}.`, "").split(".")[0]; } function removeInvalidNames(form, names) { getFieldArrayNames(form, false).forEach((fieldArrayName) => { const lastIndex = getFieldArrayStore(form, fieldArrayName).items.peek().length - 1; names.filter( (name) => name.startsWith(`${fieldArrayName}.`) && getPathIndex(fieldArrayName, name) > lastIndex ).forEach((name) => { names.splice(names.indexOf(name), 1); }); }); } function getFieldArrayNames(form, shouldValid = true) { const fieldArrayNames = [...form.internal.fieldArrayNames.peek()]; if (shouldValid) { removeInvalidNames(form, fieldArrayNames); } return fieldArrayNames; } function getFieldArrayState(form, name) { const fieldArray = getFieldArrayStore(form, name); return fieldArray ? { startItems: fieldArray.startItems.peek(), items: fieldArray.items.peek(), error: fieldArray.error.peek(), touched: fieldArray.touched.peek(), dirty: fieldArray.dirty.peek() } : void 0; } function getFieldNames(form, shouldValid = true) { const fieldNames = [...form.internal.fieldNames.peek()]; if (shouldValid) { removeInvalidNames(form, fieldNames); } return fieldNames; } function getFieldStore(form, name) { return form.internal.fields[name]; } function getFieldState(form, name) { const field = getFieldStore(form, name); return field ? { startValue: field.startValue.peek(), value: field.value.peek(), error: field.error.peek(), touched: field.touched.peek(), dirty: field.dirty.peek() } : void 0; } function getFilteredNames(form, arg2, shouldValid) { const allFieldNames = getFieldNames(form, shouldValid); const allFieldArrayNames = getFieldArrayNames(form, shouldValid); if (typeof arg2 === "string" || Array.isArray(arg2)) { return (typeof arg2 === "string" ? [arg2] : arg2).reduce( (tuple, name) => { const [fieldNames, fieldArrayNames] = tuple; if (allFieldArrayNames.includes(name)) { allFieldArrayNames.forEach((fieldArrayName) => { if (fieldArrayName.startsWith(name)) { fieldArrayNames.add(fieldArrayName); } }); allFieldNames.forEach((fieldName) => { if (fieldName.startsWith(name)) { fieldNames.add(fieldName); } }); } else { fieldNames.add(name); } return tuple; }, [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()] ).map((set) => [...set]); } return [allFieldNames, allFieldArrayNames]; } function getOptions(arg1, arg2) { return (typeof arg1 !== "string" && !Array.isArray(arg1) ? arg1 : arg2) || {}; } function getPathValue(path, object) { return path.split(".").reduce((value2, key) => value2?.[key], object); } let counter = 0; function getUniqueId() { return counter++; } function isFieldDirty(startValue, currentValue) { const toValue = (item) => item instanceof Blob ? item.size : item; return Array.isArray(startValue) && Array.isArray(currentValue) ? startValue.map(toValue).join() !== currentValue.map(toValue).join() : startValue instanceof Date && currentValue instanceof Date ? startValue.getTime() !== currentValue.getTime() : Number.isNaN(startValue) && Number.isNaN(currentValue) ? false : startValue !== currentValue; } function updateFormDirty(form, dirty) { form.dirty.value = dirty || getFieldAndArrayStores(form).some( (fieldOrFieldArray) => fieldOrFieldArray.active.peek() && fieldOrFieldArray.dirty.peek() ); } function updateFieldDirty(form, field) { const dirty = isFieldDirty( field.startValue.peek(), field.value.peek() ); if (dirty !== field.dirty.peek()) { batch(() => { field.dirty.value = dirty; updateFormDirty(form, dirty); }); } } function validateIfRequired(form, fieldOrFieldArray, name, { on: modes, shouldFocus = false }) { const validateOn = fieldOrFieldArray.validateOn ?? form.internal.validateOn; const revalidateOn = fieldOrFieldArray.revalidateOn ?? form.internal.revalidateOn; if (modes.includes( (validateOn === "submit" ? form.submitted.peek() : fieldOrFieldArray.error.peek()) ? revalidateOn : validateOn )) { validate(form, name, { shouldFocus }); } } function handleFieldEvent(form, field, name, event, validationModes, inputValue) { batch(() => { field.value.value = field.transform.reduce( (current, transformation) => transformation(current, event), inputValue ?? field.value.value ); field.touched.value = true; form.touched.value = true; updateFieldDirty(form, field); validateIfRequired(form, field, name, { on: validationModes }); }); } function initializeFieldArrayStore(form, name) { if (!getFieldArrayStore(form, name)) { const initialItems = getPathValue(name, form.internal.initialValues)?.map( () => getUniqueId() ) || []; form.internal.fieldArrays[name] = { // Signals initialItems: signal(initialItems), startItems: signal(initialItems), items: signal(initialItems), error: signal(""), active: signal(false), touched: signal(false), dirty: signal(false), // Other validate: [], validateOn: void 0, revalidateOn: void 0, consumers: /* @__PURE__ */ new Set() }; form.internal.fieldArrayNames.value = [ ...form.internal.fieldArrayNames.peek(), name ]; } return getFieldArrayStore(form, name); } function initializeFieldStore(form, name) { if (!getFieldStore(form, name)) { const initialValue = getPathValue(name, form.internal.initialValues); form.internal.fields[name] = { // Signals elements: signal([]), initialValue: signal(initialValue), startValue: signal(initialValue), value: signal(initialValue), error: signal(""), active: signal(false), touched: signal(false), dirty: signal(false), // Other validate: [], validateOn: void 0, revalidateOn: void 0, transform: [], consumers: /* @__PURE__ */ new Set() }; form.internal.fieldNames.value = [...form.internal.fieldNames.peek(), name]; } return getFieldStore(form, name); } function readSignal(signal2, peek) { return peek ? signal2.peek() : signal2.value; } function setErrorResponse(form, formErrors, { shouldActive = true }) { const message = Object.entries(formErrors).reduce((errors, [name, error]) => { if ([ getFieldStore(form, name), getFieldArrayStore(form, name) ].every( (fieldOrFieldArray) => !fieldOrFieldArray || shouldActive && !fieldOrFieldArray.active.peek() )) { errors.push(error); } return errors; }, []).join(" "); if (message) { form.response.value = { status: "error", message }; } } function setFieldArrayState(form, name, state) { const fieldArray = initializeFieldArrayStore(form, name); fieldArray.startItems.value = state.startItems; fieldArray.items.value = state.items; fieldArray.error.value = state.error; fieldArray.touched.value = state.touched; fieldArray.dirty.value = state.dirty; } function setFieldState(form, name, state) { const field = initializeFieldStore(form, name); field.startValue.value = state.startValue; field.value.value = state.value; field.error.value = state.error; field.touched.value = state.touched; field.dirty.value = state.dirty; } function setFieldArrayValue(form, name, { at: index, value: value2 }) { batch(() => { const updateStores = (prevPath, data) => { Object.entries(data).forEach(([path, value22]) => { const compoundPath = `${prevPath}.${path}`; if (!value22 || typeof value22 !== "object" || Array.isArray(value22) || value22 instanceof Date || value22 instanceof Blob) { setFieldState(form, compoundPath, { startValue: value22, value: value22, error: "", touched: false, dirty: false }); } if (Array.isArray(value22)) { const items = value22.map(() => getUniqueId()); setFieldArrayState( form, compoundPath, { startItems: [...items], items, error: "", touched: false, dirty: false } ); } if (value22 && typeof value22 === "object") { updateStores(compoundPath, value22); } }); }; updateStores(name, { [index]: value2 }); }); } function sortArrayPathIndex(name) { return (pathA, pathB) => getPathIndex(name, pathA) - getPathIndex(name, pathB); } function updateFieldArrayDirty(form, fieldArray) { const dirty = fieldArray.startItems.peek().join() !== fieldArray.items.peek().join(); if (dirty !== fieldArray.dirty.peek()) { batch(() => { fieldArray.dirty.value = dirty; updateFormDirty(form, dirty); }); } } function updateFormInvalid(form, invalid) { form.invalid.value = invalid || getFieldAndArrayStores(form).some( (fieldOrFieldArray) => fieldOrFieldArray.active.peek() && fieldOrFieldArray.error.peek() ); } function updateFormState(form) { let touched = false, dirty = false, invalid = false; for (const fieldOrFieldArray of getFieldAndArrayStores(form)) { if (fieldOrFieldArray.active.peek()) { if (fieldOrFieldArray.touched.peek()) { touched = true; } if (fieldOrFieldArray.dirty.peek()) { dirty = true; } if (fieldOrFieldArray.error.peek()) { invalid = true; } } if (touched && dirty && invalid) { break; } } batch(() => { form.touched.value = touched; form.dirty.value = dirty; form.invalid.value = invalid; }); } function focus(form, name) { getFieldStore(form, name)?.elements.peek()[0]?.focus(); } function setError(form, name, error, { shouldActive = true, shouldTouched = false, shouldDirty = false, shouldFocus = !!error } = {}) { batch(() => { for (const fieldOrFieldArray of [ getFieldStore(form, name), getFieldArrayStore(form, name) ]) { if (fieldOrFieldArray && (!shouldActive || fieldOrFieldArray.active.peek()) && (!shouldTouched || fieldOrFieldArray.touched.peek()) && (!shouldDirty || fieldOrFieldArray.dirty.peek())) { fieldOrFieldArray.error.value = error; if (error && "value" in fieldOrFieldArray && shouldFocus) { focus(form, name); } } } updateFormInvalid(form, !!error); }); } function clearError(form, name, options) { setError(form, name, "", options); } function clearResponse(form) { form.response.value = {}; } function getError(form, name, { shouldActive = true, shouldTouched = false, shouldDirty = false } = {}) { const field = getFieldStore(form, name); const fieldArray = getFieldArrayStore( form, name ); for (const fieldOrFieldArray of [field, fieldArray]) { if (fieldOrFieldArray && (!shouldActive || fieldOrFieldArray.active.value) && (!shouldTouched || fieldOrFieldArray.touched.value) && (!shouldDirty || fieldOrFieldArray.dirty.value)) { return fieldOrFieldArray.error.value; } } if (!field && !fieldArray) { form.internal.fieldNames.value; form.internal.fieldArrayNames.value; } return void 0; } function getErrors(form, arg2, arg3) { const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2); const { shouldActive = true, shouldTouched = false, shouldDirty = false } = getOptions(arg2, arg3); if (typeof arg2 !== "string" && !Array.isArray(arg2)) { form.internal.fieldNames.value; form.internal.fieldArrayNames.value; } else { fieldArrayNames.forEach( (fieldArrayName) => getFieldArrayStore(form, fieldArrayName).items.value ); } return [ ...fieldNames.map((name) => [name, getFieldStore(form, name)]), ...fieldArrayNames.map( (name) => [name, getFieldArrayStore(form, name)] ) ].reduce( (formErrors, [name, fieldOrFieldArray]) => { if (fieldOrFieldArray.error.value && (!shouldActive || fieldOrFieldArray.active.value) && (!shouldTouched || fieldOrFieldArray.touched.value) && (!shouldDirty || fieldOrFieldArray.dirty.value)) { formErrors[name] = fieldOrFieldArray.error.value; } return formErrors; }, {} ); } function getValue(form, name, { shouldActive = true, shouldTouched = false, shouldDirty = false, shouldValid = false } = {}) { const field = initializeFieldStore(form, name); if ((!shouldActive || field.active.value) && (!shouldTouched || field.touched.value) && (!shouldDirty || field.dirty.value) && (!shouldValid || !field.error.value)) { return field.value.value; } return void 0; } function getValues(form, arg2, arg3) { const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2); const { shouldActive = true, shouldTouched = false, shouldDirty = false, shouldValid = false, peek = false } = getOptions(arg2, arg3); if (!peek) { if (typeof arg2 !== "string" && !Array.isArray(arg2)) { form.internal.fieldNames.value; } else { fieldArrayNames.forEach( (fieldArrayName) => getFieldArrayStore(form, fieldArrayName).items.value ); } } return fieldNames.reduce( (values, name) => { const field = getFieldStore(form, name); if ((!shouldActive || readSignal(field.active, peek)) && (!shouldTouched || readSignal(field.touched, peek)) && (!shouldDirty || readSignal(field.dirty, peek)) && (!shouldValid || !readSignal(field.error, peek))) { (typeof arg2 === "string" ? name.replace(`${arg2}.`, "") : name).split(".").reduce( (object, key, index, keys) => object[key] = index === keys.length - 1 ? ( // If it is last key, add value readSignal(field.value, peek) ) : ( // Otherwise return object or array typeof object[key] === "object" && object[key] || (isNaN(+keys[index + 1]) ? {} : []) ), values ); } return values; }, typeof arg2 === "string" ? [] : {} ); } function hasField(form, name, { shouldActive = true, shouldTouched = false, shouldDirty = false, shouldValid = false } = {}) { return computed(() => { const field = getFieldStore(form, name); if (!field) { form.internal.fieldNames.value; } return !!field && (!shouldActive || field.active.value) && (!shouldTouched || field.touched.value) && (!shouldDirty || field.dirty.value) && (!shouldValid || !field.error.value); }).value; } function hasFieldArray(form, name, { shouldActive = true, shouldTouched = false, shouldDirty = false, shouldValid = false } = {}) { return computed(() => { const fieldArray = getFieldArrayStore(form, name); if (!fieldArray) { form.internal.fieldArrayNames.value; } return !!fieldArray && (!shouldActive || fieldArray.active.value) && (!shouldTouched || fieldArray.touched.value) && (!shouldDirty || fieldArray.dirty.value) && (!shouldValid || !fieldArray.error.value); }).value; } function insert(form, name, options) { const fieldArray = getFieldArrayStore(form, name); if (fieldArray) { const arrayLength = fieldArray.items.peek().length; const { at: index = arrayLength, value: value2 } = options; if (index >= 0 && index <= arrayLength) { batch(() => { if (index < arrayLength) { const filterName = (value22) => value22.startsWith(`${name}.`) && getPathIndex(name, value22) >= index; const getNextIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace( `${name}.${fieldOrFieldArrayIndex}`, `${name}.${fieldOrFieldArrayIndex + 1}` ); getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name)).reverse().forEach((fieldName) => { setFieldState( form, getNextIndexName(fieldName, getPathIndex(name, fieldName)), getFieldState(form, fieldName) ); }); getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name)).reverse().forEach((fieldArrayName) => { setFieldArrayState( form, getNextIndexName( fieldArrayName, getPathIndex(name, fieldArrayName) ), getFieldArrayState(form, fieldArrayName) ); }); } setFieldArrayValue(form, name, { at: index, value: value2 }); const nextItems = [...fieldArray.items.peek()]; nextItems.splice(index, 0, getUniqueId()); fieldArray.items.value = nextItems; fieldArray.touched.value = true; form.touched.value = true; fieldArray.dirty.value = true; form.dirty.value = true; }); } setTimeout( () => validateIfRequired(form, fieldArray, name, { on: ["touched", "change"] }), 250 ); } } function move(form, name, { from: fromIndex, to: toIndex }) { const fieldArray = getFieldArrayStore(form, name); if (fieldArray) { const lastIndex = fieldArray.items.peek().length - 1; if (fromIndex >= 0 && fromIndex <= lastIndex && toIndex >= 0 && toIndex <= lastIndex && fromIndex !== toIndex) { const filterName = (value2) => { if (value2.startsWith(name)) { const fieldIndex = getPathIndex(name, value2); return fieldIndex >= fromIndex && fieldIndex <= toIndex || fieldIndex <= fromIndex && fieldIndex >= toIndex; } }; const getPrevIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace( `${name}.${fieldOrFieldArrayIndex}`, fromIndex < toIndex ? `${name}.${fieldOrFieldArrayIndex - 1}` : `${name}.${fieldOrFieldArrayIndex + 1}` ); const getToIndexName = (fieldOrFieldArrayName) => fieldOrFieldArrayName.replace( `${name}.${fromIndex}`, `${name}.${toIndex}` ); const fieldNames = getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name)); const fieldArrayNames = getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name)); if (fromIndex > toIndex) { fieldNames.reverse(); fieldArrayNames.reverse(); } const fieldStateMap = /* @__PURE__ */ new Map(); const fieldArrayStateMap = /* @__PURE__ */ new Map(); batch(() => { fieldNames.forEach((fieldName) => { const fieldState = getFieldState(form, fieldName); const fieldIndex = getPathIndex(name, fieldName); if (fieldIndex === fromIndex) { fieldStateMap.set(fieldName, fieldState); } else { setFieldState( form, getPrevIndexName(fieldName, fieldIndex), fieldState ); } }); fieldStateMap.forEach((fieldState, fieldName) => { setFieldState(form, getToIndexName(fieldName), fieldState); }); fieldArrayNames.forEach((fieldArrayName) => { const fieldArrayState = getFieldArrayState(form, fieldArrayName); const fieldArrayIndex = getPathIndex(name, fieldArrayName); if (fieldArrayIndex === fromIndex) { fieldArrayStateMap.set(fieldArrayName, fieldArrayState); } else { setFieldArrayState( form, getPrevIndexName(fieldArrayName, fieldArrayIndex), fieldArrayState ); } }); fieldArrayStateMap.forEach((fieldArrayState, fieldArrayName) => { setFieldArrayState( form, getToIndexName(fieldArrayName), fieldArrayState ); }); const nextItems = [...fieldArray.items.peek()]; nextItems.splice(toIndex, 0, nextItems.splice(fromIndex, 1)[0]); fieldArray.items.value = nextItems; fieldArray.touched.value = true; form.touched.value = true; updateFieldArrayDirty(form, fieldArray); }); } } } function remove(form, name, { at: index }) { const fieldArray = getFieldArrayStore(form, name); if (fieldArray) { const lastIndex = fieldArray.items.peek().length - 1; if (index >= 0 && index <= lastIndex) { const filterName = (value2) => value2.startsWith(`${name}.`) && getPathIndex(name, value2) > index; const getPrevIndexName = (fieldOrFieldArrayName, fieldOrFieldArrayIndex) => fieldOrFieldArrayName.replace( `${name}.${fieldOrFieldArrayIndex}`, `${name}.${fieldOrFieldArrayIndex - 1}` ); batch(() => { getFieldNames(form).filter(filterName).sort(sortArrayPathIndex(name)).forEach((fieldName) => { setFieldState( form, getPrevIndexName(fieldName, getPathIndex(name, fieldName)), getFieldState(form, fieldName) ); }); getFieldArrayNames(form).filter(filterName).sort(sortArrayPathIndex(name)).forEach((fieldArrayName) => { setFieldArrayState( form, getPrevIndexName( fieldArrayName, getPathIndex(name, fieldArrayName) ), getFieldArrayState(form, fieldArrayName) ); }); const nextItems = [...fieldArray.items.peek()]; nextItems.splice(index, 1); fieldArray.items.value = nextItems; fieldArray.touched.value = true; form.touched.value = true; updateFieldArrayDirty(form, fieldArray); validateIfRequired(form, fieldArray, name, { on: ["touched", "change"] }); }); } } } function replace(form, name, options) { const fieldArray = getFieldArrayStore(form, name); if (fieldArray) { const { at: index } = options; const lastIndex = fieldArray.items.peek().length - 1; if (index >= 0 && index <= lastIndex) { batch(() => { setFieldArrayValue(form, name, options); const nextItems = [...fieldArray.items.peek()]; nextItems[index] = getUniqueId(); fieldArray.items.value = nextItems; fieldArray.touched.value = true; form.touched.value = true; fieldArray.dirty.value = true; form.dirty.value = true; }); } } } function reset(form, arg2, arg3) { const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2, false); const resetSingleField = typeof arg2 === "string" && fieldNames.length === 1; const resetEntireForm = !resetSingleField && !Array.isArray(arg2); const options = getOptions(arg2, arg3); const { initialValue, initialValues, keepResponse = false, keepSubmitCount = false, keepSubmitted = false, keepValues = false, keepDirtyValues = false, keepItems = false, keepDirtyItems = false, keepErrors = false, keepTouched = false, keepDirty = false } = options; batch(() => { fieldNames.forEach((name) => { const field = getFieldStore(form, name); if (resetSingleField ? "initialValue" in options : initialValues) { field.initialValue.value = resetSingleField ? initialValue : getPathValue(name, initialValues); } const keepDirtyValue = keepDirtyValues && field.dirty.peek(); if (!keepValues && !keepDirtyValue) { field.startValue.value = field.initialValue.peek(); field.value.value = field.initialValue.peek(); field.elements.peek().forEach((element) => { if (element.type === "file") { element.value = ""; } }); } if (!keepTouched) { field.touched.value = false; } if (!keepDirty && !keepValues && !keepDirtyValue) { field.dirty.value = false; } if (!keepErrors) { field.error.value = ""; } }); fieldArrayNames.forEach((name) => { const fieldArray = getFieldArrayStore(form, name); const keepCurrentDirtyItems = keepDirtyItems && fieldArray.dirty.peek(); if (!keepItems && !keepCurrentDirtyItems) { if (initialValues) { fieldArray.initialItems.value = getPathValue(name, initialValues)?.map(() => getUniqueId()) || []; } fieldArray.startItems.value = [...fieldArray.initialItems.peek()]; fieldArray.items.value = [...fieldArray.initialItems.peek()]; } if (!keepTouched) { fieldArray.touched.value = false; } if (!keepDirty && !keepItems && !keepCurrentDirtyItems) { fieldArray.dirty.value = false; } if (!keepErrors) { fieldArray.error.value = ""; } }); if (resetEntireForm) { if (!keepResponse) { form.response.value = {}; } if (!keepSubmitCount) { form.submitCount.value = 0; } if (!keepSubmitted) { form.submitted.value = false; } } updateFormState(form); }); } function setResponse(form, response, { duration } = {}) { form.response.value = response; if (duration) { setTimeout(() => { if (form.response.peek() === response) { form.response.value = {}; } }, duration); } } function setValue(form, name, value2, { shouldTouched = true, shouldDirty = true, shouldValidate = true, shouldFocus = true } = {}) { batch(() => { const field = initializeFieldStore(form, name); field.value.value = value2; if (shouldTouched) { field.touched.value = true; form.touched.value = true; } if (shouldDirty) { updateFieldDirty(form, field); } if (shouldValidate) { validateIfRequired(form, field, name, { on: ["touched", "change"], shouldFocus }); } }); } async function validate(form, arg2, arg3) { const [fieldNames, fieldArrayNames] = getFilteredNames(form, arg2); const { shouldActive = true, shouldFocus = true } = getOptions(arg2, arg3); const validator = getUniqueId(); form.internal.validators.add(validator); form.validating.value = true; const formErrors = form.internal.validate ? await form.internal.validate( getValues(form, { shouldActive, peek: true }) ) : {}; let valid = typeof arg2 !== "string" && !Array.isArray(arg2) ? !Object.keys(formErrors).length : true; const [errorFields] = await Promise.all([ // Validate each field in list Promise.all( fieldNames.map(async (name) => { const field = getFieldStore(form, name); if (!shouldActive || field.active.peek()) { let localError; for (const validation of field.validate) { localError = await validation(field.value.peek()); if (localError) { break; } } const fieldError = localError || formErrors[name] || ""; if (fieldError) { valid = false; } field.error.value = fieldError; return fieldError ? name : null; } }) ), // Validate each field array in list Promise.all( fieldArrayNames.map(async (name) => { const fieldArray = getFieldArrayStore(form, name); if (!shouldActive || fieldArray.active.peek()) { let localError = ""; for (const validation of fieldArray.validate) { localError = await validation(fieldArray.items.peek()); if (localError) { break; } } const fieldArrayError = localError || formErrors[name] || ""; if (fieldArrayError) { valid = false; } fieldArray.error.value = fieldArrayError; } }) ) ]); batch(() => { setErrorResponse(form, formErrors, { shouldActive }); if (shouldFocus) { const name = errorFields.find((name2) => name2); if (name) { focus(form, name); } } updateFormInvalid(form, !valid); form.internal.validators.delete(validator); if (!form.internal.validators.size) { form.validating.value = false; } }); return valid; } function setValues(form, arg2, arg3, arg4) { const isFieldArray = typeof arg2 === "string"; const values = isFieldArray ? arg3 : arg2; const options = (isFieldArray ? arg4 : arg3) || {}; const { shouldTouched = true, shouldDirty = true, shouldValidate = true, shouldFocus = true } = options; const names = isFieldArray ? [arg2] : []; batch(() => { const setFieldArrayItems = (name, value2) => { const fieldArray = initializeFieldArrayStore(form, name); fieldArray.items.value = value2.map(() => getUniqueId()); if (shouldTouched) { fieldArray.touched.value = true; form.touched.value = true; } if (shouldDirty) { fieldArray.dirty.value = true; form.dirty.value = true; } }; const setNestedValues = (data, prevPath) => Object.entries(data).forEach(([path, value2]) => { const compoundPath = prevPath ? `${prevPath}.${path}` : path; if (!value2 || typeof value2 !== "object" || !Array.isArray(value2)) { setValue(form, compoundPath, value2, { ...options, shouldValidate: false }); names.push( compoundPath ); } if (Array.isArray(value2)) { setFieldArrayItems( compoundPath, value2 ); } if (value2 && typeof value2 === "object") { setNestedValues(value2, compoundPath); } }); if (isFieldArray) { setFieldArrayItems( arg2, arg3 ); } setNestedValues(values, isFieldArray ? arg2 : void 0); if (shouldValidate && ["touched", "change"].includes( form.internal.validateOn === "submit" && form.submitted ? form.internal.revalidateOn : form.internal.validateOn )) { validate(form, names, { shouldFocus }); } }); } function submit(form) { form.element.peek()?.requestSubmit(); } function swap(form, name, { at: index1, and: index2 }) { const fieldArray = getFieldArrayStore(form, name); if (fieldArray) { const lastIndex = fieldArray.items.peek().length - 1; if (index1 >= 0 && index1 <= lastIndex && index2 >= 0 && index2 <= lastIndex && index1 !== index2) { const index1Prefix = `${name}.${index1}`; const index2Prefix = `${name}.${index2}`; const fieldStateMap = /* @__PURE__ */ new Map(); const fieldArrayStateMap = /* @__PURE__ */ new Map(); const filterName = (value2) => value2.startsWith(`${name}.`) && [index1, index2].includes(getPathIndex(name, value2)); const swapIndex = (value2) => value2.startsWith(index1Prefix) ? value2.replace(index1Prefix, index2Prefix) : value2.replace(index2Prefix, index1Prefix); getFieldNames(form).filter(filterName).forEach( (fieldName) => fieldStateMap.set(fieldName, getFieldState(form, fieldName)) ); getFieldArrayNames(form).filter(filterName).forEach( (fieldArrayName) => fieldArrayStateMap.set( fieldArrayName, getFieldArrayState(form, fieldArrayName) ) ); batch(() => { fieldStateMap.forEach( (fieldState, fieldName) => setFieldState(form, swapIndex(fieldName), fieldState) ); fieldArrayStateMap.forEach( (fieldArrayState, fieldArrayName) => setFieldArrayState(form, swapIndex(fieldArrayName), fieldArrayState) ); const nextItems = [...fieldArray.items.peek()]; nextItems[index1] = fieldArray.items.peek()[index2]; nextItems[index2] = fieldArray.items.peek()[index1]; fieldArray.items.value = nextItems; fieldArray.touched.value = true; form.touched.value = true; updateFieldArrayDirty(form, fieldArray); }); } } } function useLifecycle({ of: form, name, store, validate: validate2, validateOn, revalidateOn, transform, keepActive = false, keepState = true }) { useEffect(() => { store.validate = validate2 ? Array.isArray(validate2) ? validate2 : [validate2] : []; store.validateOn = validateOn; store.revalidateOn = revalidateOn; if ("transform" in store) { store.transform = transform ? Array.isArray(transform) ? transform : [transform] : []; } }, [store, transform, validate2, validateOn, revalidateOn]); useEffect(() => { const consumer = getUniqueId(); store.consumers.add(consumer); if (!store.active.peek()) { batch(() => { store.active.value = true; updateFormState(form); }); } return () => { setTimeout(() => { store.consumers.delete(consumer); batch(() => { if (!keepActive && !store.consumers.size) { store.active.value = false; if (!keepState) { reset(form, name); } else { updateFormState(form); } } }); if ("elements" in store) { store.elements.value = store.elements.peek().filter((element) => element.isConnected); } }); }; }, [form, name, store, keepActive, keepState]); } function useLiveSignal(value2) { const signal2 = useSignal(value2); if (signal2.peek() !== value2) signal2.value = value2; return useComputed(() => signal2.value.value); } function Field({ children, type, ...props }) { const { of: form, name } = props; const field = useMemo(() => initializeFieldStore(form, name), [form, name]); useLifecycle({ ...props, store: field }); const value2 = useLiveSignal(field.value); const error = useLiveSignal(field.error); const active = useLiveSignal(field.active); const touched = useLiveSignal(field.touched); const dirty = useLiveSignal(field.dirty); return /* @__PURE__ */ jsx(Fragment, { children: children( useMemo( () => ({ name, value: value2, error, active, touched, dirty }), // eslint-disable-next-line react-hooks/exhaustive-deps [name] ), useMemo( () => ({ name, ref(element) { if (element) { field.elements.value = [...field.elements.value, element]; effect(() => { if (element.type !== "radio" && field.startValue.value === void 0 && field.value.peek() === void 0) { setTimeout(() => { const input = getElementInput(element, field, type); field.startValue.value = input; field.value.value = input; }); } }); } }, onChange(event) { handleFieldEvent( form, field, name, event, ["touched", "change"], getElementInput(event.currentTarget, field, type) ); }, onBlur(event) { handleFieldEvent(form, field, name, event, ["touched", "blur"]); } }), [field, form, name, type] ) ) }); } function FieldArray({ children, ...props }) { const { of: form, name } = props; const fieldArray = useMemo( () => initializeFieldArrayStore(form, name), [form, name] ); useLifecycle({ ...props, store: fieldArray }); const items = useLiveSignal(fieldArray.items); const error = useLiveSignal(fieldArray.error); const active = useLiveSignal(fieldArray.active); const touched = useLiveSignal(fieldArray.touched); const dirty = useLiveSignal(fieldArray.dirty); return /* @__PURE__ */ jsx(Fragment, { children: children( useMemo( () => ({ name, items, error, active, touched, dirty }), // eslint-disable-next-line react-hooks/exhaustive-deps [name] ) ) }); } class FormError extends Error { constructor(arg1, arg2) { super(typeof arg1 === "string" ? arg1 : ""); __publicField(this, "name", "FormError"); __publicField(this, "errors"); this.errors = typeof arg1 === "string" ? arg2 || {} : arg1; } } function Form({ of: form, onSubmit, responseDuration: duration, keepResponse, shouldActive, shouldTouched, shouldDirty, shouldFocus, ...props }) { return /* @__PURE__ */ jsx( "form", { noValidate: true, ...props, ref: (element) => form.element.value = element, onSubmit: async (event) => { event.preventDefault(); batch(() => { if (!keepResponse) { form.response.value = {}; } form.submitCount.value++; form.submitted.value = true; form.submitting.value = true; }); const options = { duration, shouldActive, shouldTouched, shouldDirty, shouldFocus }; try { if (await validate(form, options)) { await onSubmit?.(getValues(form, options), event); } } catch (error) { batch(() => { if (error instanceof FormError) { Object.entries(error.errors).forEach(([name, error2]) => { if (error2) { setError(form, name, error2, { ...options, shouldFocus: false }); } }); } if (!(error instanceof FormError) || error.message) { setResponse( form, { status: "error", message: error?.message || "An unknown error has occurred." }, options ); } }); } finally { form.submitting.value = false; } } } ); } function toCustom(action, { on: mode }) { return (value2, event) => event.type === mode ? action(value2, event) : value2; } function toLowerCase(options) { return toCustom( (value2) => value2 && value2.toLowerCase(), options ); } function toTrimmed(options) { return toCustom( (value2) => value2 && value2.trim(), options ); } function toUpperCase(options) { return toCustom( (value2) => value2 && value2.toUpperCase(), options ); } function custom(requirement, error) { return async (value2) => (Array.isArray(value2) ? value2.length : value2 || value2 === 0) && !await requirement(value2) ? error : ""; } function email(error) { return (value2) => value2 && !/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test( value2 ) ? error : ""; } function maxLength(requirement, error) { return (value2) => value2?.length && value2.length > requirement ? error : ""; } function maxRange(requirement, error) { return (value2) => (value2 || value2 === 0) && value2 > requirement ? error : ""; } function maxSize(requirement, error) { return (value2) => value2 && (Array.isArray(value2) ? [...value2].some((file) => file.size > requirement) : value2.size > requirement) ? error : ""; } function maxTotalSize(requirement, error) { return (value2) => value2?.length && [...value2].reduce((size, file) => size + file.size, 0) > requirement ? error : ""; } function mimeType(requirement, error) { const mimeTypes = Array.isArray(requirement) ? requirement : [requirement]; return (value2) => value2 && (Array.isArray(value2) ? [...value2].some((file) => !mimeTypes.includes(file.type)) : !mimeTypes.includes(value2.type)) ? error : ""; } function minLength(requirement, error) { return (value2) => value2?.length && value2.length < requirement ? error : ""; } function minRange(requirement, error) { return (value2) => (value2 || value2 === 0) && value2 < requirement ? error : ""; } function minSize(requirement, error) { return (value2) => value2 && (Array.isArray(value2) ? [...value2].some((file) => file.size < requirement) : value2.size < requirement) ? error : ""; } function minTotalSize(requirement, error) { return (value2) => value2?.length && [...value2].reduce((size, file) => size + file.size, 0) < requirement ? error : ""; } function pattern(requirement, error) { return (value2) => value2 && !requirement.test(value2) ? error : ""; } function required(error) { return (value2) => !value2 && value2 !== 0 || Array.isArray(value2) && !value2.length ? error : ""; } function url(error) { return (value2) => { try { value2 && new URL(value2); return ""; } catch (_) { return error; } }; } function value(requirement, error) { return (value2) => (value2 || value2 === 0) && value2 !== requirement ? error : ""; } export { Field, FieldArray, Form, FormError, clearError, clearResponse, custom, email, focus, getError, getErrors, getValue, getValues, hasField, hasFieldArray, insert, maxLength, maxRange, maxSize, maxTotalSize, mimeType, minLength, minRange, minSize, minTotalSize, move, pattern, remove, replace, required, reset, setError, setResponse, setValue, setValues, submit, swap, toCustom, toLowerCase, toTrimmed, toUpperCase, url, useForm, useFormStore, valiField, valiForm, validate, value, zodField, zodForm };