UNPKG

formango

Version:
854 lines (848 loc) 27.1 kB
import deepClone from "clone-deep"; import { computed, getCurrentInstance, nextTick, onUnmounted, ref, shallowReactive, toValue, watch } from "vue"; import { setupDevtoolsPlugin } from "@vue/devtools-api"; //#region src/lib/formatErrors.ts function issueMapper(issue) { return issue.message; } function getNormalizedPathArray(issue) { if (typeof issue.path === "object") return issue.path?.map((item) => typeof item === "object" ? item.key : item); return issue.path; } function formatErrorsToZodFormattedError(issues) { const fieldErrors = { _errors: [] }; function processIssue(issue) { if (issue.path == null || issue.path?.length === 0) { fieldErrors._errors.push(issueMapper(issue)); return; } const normalizedPath = getNormalizedPathArray(issue); let curr = fieldErrors; let i = 0; while (i < normalizedPath.length) { const el = normalizedPath[i]; if (!(i === normalizedPath.length - 1)) curr[el] = curr[el] || { _errors: [] }; else { curr[el] = curr[el] || { _errors: [] }; curr[el]._errors.push(issueMapper(issue)); } curr = curr[el]; i++; } } for (const issue of issues) processIssue(issue); return fieldErrors; } //#endregion //#region src/utils/index.ts function isObject(value) { return value !== null && typeof value === "object"; } function isNullOrUndefined(value) { return value === null || value === void 0; } function isUndefined(val) { return val === void 0; } function isEmptyArray(obj) { for (const key in obj) if (!isUndefined(obj[key])) return false; return true; } function isEmptyObject(value) { return isObject(value) && Object.keys(value).length === 0; } function baseGet(object, updatePath) { const length = updatePath.slice(0, -1).length; let index = 0; while (index < length) object = isUndefined(object) ? index++ : object[updatePath[index++]]; return object; } function set(object, path, value) { let index = -1; const arrayPath = path.split("."); const length = arrayPath.length; const lastIndex = length - 1; while (++index < length) { const key = arrayPath[index]; let newValue = value; if (index !== lastIndex) { const objValue = object[key]; newValue = isObject(objValue) || Array.isArray(objValue) ? objValue : !Number.isNaN(+arrayPath[index + 1]) ? [] : {}; } object[key] = newValue; object = object[key]; } return object; } function get(obj, path, defaultValue) { const result = path.split(".").reduce((result$1, key) => isNullOrUndefined(result$1) ? result$1 : result$1[key], obj); if (isNullOrUndefined(obj)) return; return isUndefined(result) || result === obj ? isUndefined(obj[path]) ? defaultValue : obj[path] : result; } function unset(object, path) { const arrayPath = path.split("."); const childObject = arrayPath.length === 1 ? object : baseGet(object, arrayPath); const index = arrayPath.length - 1; const key = arrayPath[index]; if (childObject) { if (Array.isArray(childObject)) childObject.splice(+key, 1); else if (isObject(childObject)) { const value = childObject[key]; if (!Array.isArray(value)) delete childObject[key]; } } if (index !== 0 && (isObject(childObject) && isEmptyObject(childObject) || Array.isArray(childObject) && isEmptyArray(childObject))) unset(object, arrayPath.slice(0, -1).join(".")); return object; } function generateId() { let id = ""; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (let i = 0; i < 10; i += 1) id += chars.charAt(Math.floor(Math.random() * 62)); return `form-${id}`; } function throttle(func, limit) { let inThrottle; let lastResult; return function(...args) { const context = this; if (!inThrottle) { inThrottle = true; setTimeout(() => inThrottle = false, limit); lastResult = func.apply(context, args); } return lastResult; }; } function isSubPath({ childPath, parentPath }) { const childSegments = childPath.split("."); const parentSegments = parentPath.split("."); if (childSegments.length <= parentSegments.length) return { isPart: false }; for (const [i, parentSegment] of parentSegments.entries()) if (childSegments[i] !== parentSegment) return { isPart: false }; return { isPart: true, relativePath: childSegments.slice(parentSegments.length).join(".") }; } //#endregion //#region src/devtools/devtoolsBuilder.ts function buildFormState(form) { return { "Form state": [ { key: "state", value: form.state }, { key: "errors", value: form.errors }, { key: "isDirty", value: form.isDirty }, { key: "hasAttemptedToSubmit", value: form.hasAttemptedToSubmit }, { key: "isSubmitting", value: form.isSubmitting }, { key: "isValid", value: form.isValid } ] }; } function buildFieldState(field) { return { "Field state": [ { key: "value", value: field.modelValue }, { key: "path", value: field._path }, { key: "errors", value: field.errors }, { key: "isChanged", value: field.isChanged }, { key: "isDirty", value: field.isDirty }, { key: "isTouched", value: field.isTouched } ] }; } //#endregion //#region src/devtools/devtools.ts let API; const INSPECTOR_ID = "formango-inspector"; const DEVTOOLS_FORMS = ref({}); const DEVTOOLS_FIELDS = ref({}); const COLORS = { black: 0, blue: 218007, error: 12405579, gray: 12304330, orange: 16099682, purple: 12157168, success: 448379, unknown: 5522283, white: 16777215 }; let IS_INSTALLED = false; function mapFieldsToObject(fields) { const obj = {}; for (const field of fields) { if (!field._path) continue; const pathArray = field._path?.split("."); if (!pathArray) continue; const lastKey = pathArray.pop(); const lastObj = pathArray.reduce((obj$1, key) => obj$1[key] = obj$1[key] || {}, obj); if (!lastObj[lastKey]) lastObj[lastKey] = {}; lastObj[lastKey].__FIELD__ = field; } return obj; } let nonFieldsCounter = 0; function mapObjectToCustomInspectorNode(obj) { return Object.keys(obj).map((key) => { const value = obj[key]; if (value.__FIELD__) { const field = value.__FIELD__; const hasError = field.errors && Object.values(field.errors).length > 0; const validTag = { backgroundColor: hasError ? COLORS.error : COLORS.success, label: hasError ? "Invalid" : "Valid", textColor: COLORS.white }; const tags = []; if (hasError) tags.push(validTag); delete value.__FIELD__; return { id: field.__ID__, label: key, tags, children: mapObjectToCustomInspectorNode(value) }; } else { nonFieldsCounter++; return { id: `non-field-${nonFieldsCounter}`, label: key, tags: [{ backgroundColor: COLORS.orange, label: "Not registered", textColor: COLORS.white }], children: mapObjectToCustomInspectorNode(value) }; } }); } function calculateNodes() { nonFieldsCounter = 0; return Object.keys(DEVTOOLS_FORMS.value).map((formId) => { const form = DEVTOOLS_FORMS.value[formId]; const actualForm = form.form; const formChildren = mapObjectToCustomInspectorNode(mapFieldsToObject(Object.keys(DEVTOOLS_FIELDS.value).filter((key) => { const field = DEVTOOLS_FIELDS.value[key]; return form.form._id === field.formId; }).map((key) => { const field = DEVTOOLS_FIELDS.value[key]; field.field.__ID__ = key; return field.field; }))); const validTag = { backgroundColor: actualForm.isValid ? COLORS.success : COLORS.error, label: actualForm.isValid ? "Valid" : "Invalid", textColor: COLORS.white }; return { id: formId, label: `${form.name}`, tags: [validTag], children: formChildren }; }); } const refreshInspector = throttle(() => { setTimeout(async () => { await nextTick(); API?.sendInspectorState(INSPECTOR_ID); API?.sendInspectorTree(INSPECTOR_ID); }, 100); }, 100); const isDevMode = process.env.NODE_ENV === "development"; function installDevtoolsPlugin(app) { if (!isDevMode) return; setupDevtoolsPlugin({ id: "formango-devtools-plugin", app, homepage: "https://github.com/wisemen-digital/vue-formango", label: "Formango Plugin", logo: "https://wisemen-digital.github.io/vue-formango/assets/mango_no_shadow.svg", packageName: "formango" }, setupApiHooks); } function setupApiHooks(api) { API = api; api.addInspector({ id: INSPECTOR_ID, icon: "rule", label: "formango", noSelectionText: "Select a form node to inspect" }); api.on.getInspectorTree((payload) => { if (payload.inspectorId !== INSPECTOR_ID) return; try { payload.rootNodes = calculateNodes(); } catch (error) { console.error("Error with calculating devtools nodes"); console.error(error); } }); api.on.getInspectorState((payload) => { if (payload.inspectorId !== INSPECTOR_ID) return; const decodedNode = decodeNodeId(payload.nodeId); if (decodedNode?.type === "form" && decodedNode?.form) payload.state = buildFormState(decodedNode.form); else if (decodedNode?.type === "field" && decodedNode?.field?.field) payload.state = buildFieldState(decodedNode?.field.field); }); } function installPlugin() { if (!isDevMode) return; const vm = getCurrentInstance(); if (!IS_INSTALLED) { IS_INSTALLED = true; const app = vm?.appContext.app; if (!app) return; installDevtoolsPlugin(app); } } function registerFormWithDevTools(form, name) { if (!isDevMode) return; installPlugin(); if (!form?._id) return; const componentName = getCurrentInstance()?.type.__name; const encodedForm = encodeNodeId({ id: form._id, name: name ?? "Unknown form", type: "form" }); DEVTOOLS_FORMS.value[encodedForm] = { name: componentName ?? "Unknown form", form }; onUnmounted(() => { const formFields = Object.keys(DEVTOOLS_FIELDS.value).filter((fieldId) => { return DEVTOOLS_FIELDS.value[fieldId].formId === form?._id; }); delete DEVTOOLS_FORMS.value[encodedForm]; formFields.forEach((formFieldId) => { delete DEVTOOLS_FIELDS.value[formFieldId]; }); }); } function registerFieldWithDevTools(formId, field) { if (!isDevMode) return; installPlugin(); const encodedField = encodeNodeId({ id: field._id, type: "field" }); DEVTOOLS_FIELDS.value[encodedField] = { formId, field }; } function unregisterFieldWithDevTools(fieldId) { if (!isDevMode) return; const encodedField = encodeNodeId({ id: fieldId, type: "field" }); delete DEVTOOLS_FIELDS.value[encodedField]; } function encodeNodeId(node) { return btoa(encodeURIComponent(JSON.stringify(node))); } function decodeNodeId(nodeId) { try { const decodedNode = JSON.parse(decodeURIComponent(atob(nodeId))); if (!decodedNode) throw new Error("Invalid node id"); if (decodedNode.type === "form" && DEVTOOLS_FORMS.value[nodeId]) return { name: decodedNode.name, form: DEVTOOLS_FORMS.value[nodeId].form, type: "form" }; else return { field: DEVTOOLS_FIELDS.value[nodeId], type: "field" }; } catch {} return null; } if (isDevMode) watch([DEVTOOLS_FORMS.value, DEVTOOLS_FIELDS.value], refreshInspector, { deep: true }); //#endregion //#region src/lib/useForm.ts function useForm({ initialState, schema, onSubmit, onSubmitError }) { const formId = generateId(); const form = ref({}); const rawErrors = ref([]); const formattedErrors = computed(() => { return rawErrors.value.map((error) => { if (error.path == null) return error; const mappedPath = error.path?.map((item) => typeof item === "object" ? item.key : item).join("."); return { message: error.message, path: mappedPath }; }); }); const onSubmitCb = onSubmit; const onSubmitFormErrorCb = onSubmitError; const isSubmitting = ref(false); const hasAttemptedToSubmit = ref(false); const initialFormState = ref(initialState ? deepClone(toValue(initialState)) : null); const paths = ref(/* @__PURE__ */ new Map()); const trackedDependencies = /* @__PURE__ */ new Map(); const registeredFields = shallowReactive(/* @__PURE__ */ new Map()); const registeredFieldArrays = shallowReactive(/* @__PURE__ */ new Map()); if (initialState != null) Object.assign(form.value, deepClone(toValue(initialState))); const isDirty = computed(() => { return [...registeredFields.values(), ...registeredFieldArrays.values()].some((field) => toValue(field.isDirty)); }); const isValid = computed(() => { return rawErrors.value.length === 0; }); watch(() => toValue(initialState), (newInitialState) => { if (!isDirty.value && newInitialState != null) { initialFormState.value = deepClone(toValue(newInitialState)); Object.assign(form.value, deepClone(toValue(newInitialState))); } }, { deep: true }); function getIdByPath(paths$1, path) { return [...paths$1.entries()].find(([, p]) => p === path)?.[0] ?? null; } function updatePaths(path) { if (!Number.isNaN(path.split(".").pop())) { const index = Number.parseInt(path.split(".").pop() ?? "0", 10); const parentPath = path.split(".").slice(0, -1).join("."); const matchingPaths = [...paths.value.entries()].filter(([, p]) => p.startsWith(parentPath)); for (const [id, p] of matchingPaths) { if (!p.startsWith(`${parentPath}.`)) continue; const i = Number.parseInt(p.replace(`${parentPath}.`, ""), 10); if (i > index) { const newPath = `${parentPath}.${i - 1}`; const suffixPath = p.slice(newPath.length); paths.value.set(id, `${newPath}${suffixPath}`); } else if (i === index) paths.value.delete(id); } } else { const id = getIdByPath(paths.value, path) ?? null; if (id === null) throw new Error("Path not found"); paths.value.delete(id); } } function getChildPaths(path) { return [...registeredFields.values(), ...registeredFieldArrays.values()].filter((field) => { if (field._path.value == null) return false; const { isPart } = isSubPath({ childPath: field._path.value, parentPath: path }); return isPart; }); } function createField(id, path, defaultOrExistingValue) { const field = { "_id": id, "isChanged": ref(false), "isDirty": computed(() => false), "isTouched": computed(() => false), "isValid": computed(() => false), "_isTouched": ref(false), "_path": computed(() => path), "blurAll": () => { field._isTouched.value = true; for (const registeredField of [...registeredFields.values()].filter((registeredField$1) => { if (field._path.value == null || registeredField$1._path.value == null) return false; const { isPart } = isSubPath({ childPath: registeredField$1._path.value, parentPath: field._path.value }); return isPart; })) registeredField.blurAll(); }, "errors": computed(() => []), "modelValue": computed(() => defaultOrExistingValue), "rawErrors": computed(() => []), "register": (childPath, defaultValue) => { return register(`${paths.value.get(id)}.${childPath}`, defaultValue); }, "registerArray": (childPath, defaultValue) => { return registerArray(`${paths.value.get(id)}.${childPath}`, defaultValue); }, "setValue": (newValue) => { field["onUpdate:modelValue"](newValue); }, "value": computed(() => defaultOrExistingValue), "onBlur": () => { field._isTouched.value = true; }, "onChange": () => { field.isChanged.value = true; }, "onUpdate:modelValue": (newValue) => { if (field._path.value === null) return; set(form.value, field._path.value, newValue); } }; return field; } function createFieldArray(id, path, defaultOrExistingValue) { const fields = ref([]); for (let i = 0; i < defaultOrExistingValue.length; i++) { const fieldId = generateId(); fields.value.push(fieldId); } function insert(index, value) { const path$1 = paths.value.get(id); fields.value[index] = generateId(); return register(`${path$1}.${index}`, value); } function remove(index) { const currentPath = paths.value.get(id); fields.value.splice(index, 1); unregister(`${currentPath}.${index}`); } function prepend(value) { insert(0, value); } function append(value) { return insert(fields.value.length, value); } function pop() { remove(fields.value.length - 1); } function shift() { remove(0); } function move(from, to) { [fields.value[from], fields.value[to]] = [fields.value[to], fields.value[from]]; const currentPath = paths.value.get(id); const currentValue = get(form.value, currentPath); const value = currentValue[from]; currentValue[from] = currentValue[to]; currentValue[to] = value; set(form.value, currentPath, currentValue); const fromPath = `${currentPath}.${from}`; const toPath = `${currentPath}.${to}`; const fromId = getIdByPath(paths.value, fromPath); const toId = getIdByPath(paths.value, toPath); if (fromId === null || toId === null) throw new Error("Path not found"); for (const [id$1, p] of paths.value.entries()) if (p.startsWith(fromPath)) { const newPath = p.replace(fromPath, toPath); paths.value.set(id$1, newPath); } else if (p.startsWith(toPath)) { const newPath = p.replace(toPath, fromPath); paths.value.set(id$1, newPath); } paths.value.set(fromId, toPath); paths.value.set(toId, fromPath); } function empty() { for (let i = fields.value.length - 1; i >= 0; i--) remove(i); } function setValue(value) { empty(); for (const arrayValue of value) append(arrayValue); } const fieldArray = { _id: id, isDirty: computed(() => false), isTouched: computed(() => false), isValid: computed(() => false), _path: computed(() => path), append, blurAll: () => { for (const registeredField of [...registeredFields.values()].filter((registeredField$1) => { if (fieldArray._path.value == null || registeredField$1._path.value == null) return false; const { isPart } = isSubPath({ childPath: registeredField$1._path.value, parentPath: fieldArray._path.value }); return isPart; })) registeredField.blurAll(); }, empty, errors: computed(() => []), fields, insert, modelValue: computed(() => defaultOrExistingValue), move, pop, prepend, rawErrors: computed(() => []), register: (childPath, defaultValue) => { const fullPath = `${paths.value.get(id)}.${childPath}`; for (let i = 0; i <= Number(childPath.split(".").pop()); i += 1) if (fields.value[i] === void 0) fields.value[i] = generateId(); return register(fullPath, defaultValue); }, registerArray: (childPath, defaultValue) => { const fullPath = `${paths.value.get(id)}.${childPath}`; for (let i = 0; i <= Number(childPath.split(".").pop()); i += 1) if (fields.value[i] === void 0) fields.value[i] = generateId(); return registerArray(fullPath, defaultValue); }, remove, setValue, shift, value: computed(() => defaultOrExistingValue) }; return fieldArray; } function isField(field) { return field._isTouched !== void 0; } function getFieldWithTrackedDependencies(field, initialValue) { const parsedStringifiedInitialValue = JSON.parse(JSON.stringify(initialValue)); field._path = computed(() => { return paths.value.get(field._id) ?? null; }); field.modelValue = computed(() => { if (field._path.value === null) return null; return get(form.value, field._path.value); }); field.value = computed(() => toValue(field.modelValue.value)); field.isValid = computed(() => { if (field._path.value === null) return false; return field.rawErrors.value.length === 0; }); field.isDirty = computed(() => { if (field._path.value === null) return false; const initialValue$1 = get(initialFormState.value, field._path.value) ?? parsedStringifiedInitialValue; if (field.modelValue.value === "" && initialValue$1 === null) return false; return JSON.stringify(field.modelValue.value) !== JSON.stringify(initialValue$1); }); field.isTouched = computed(() => { if (field._path.value === null) return false; if (getChildPaths(field._path.value).some((child) => child.isTouched.value)) return true; if (isField(field)) return field._isTouched.value; return false; }); field.rawErrors = computed(() => { if (field._path.value === null) return []; return rawErrors.value.filter((error) => { const dottedPath = error.path?.map((item) => typeof item === "object" ? item.key : item).join("."); if (dottedPath == null || field._path.value == null) return false; const { isPart } = isSubPath({ childPath: dottedPath, parentPath: field._path.value }); if (dottedPath === field._path.value) return true; return isPart; }).map((error) => { const normalizedPath = error.path?.map((item) => typeof item === "object" ? item.key : item); if (normalizedPath == null || field._path.value == null) return error; const { isPart, relativePath } = isSubPath({ childPath: normalizedPath.join("."), parentPath: field._path.value }); if (!isPart) return { ...error, path: [] }; return { ...error, path: relativePath?.split(".") ?? [] }; }); }); field.errors = computed(() => { if (field._path.value === null) return []; return rawErrors.value.filter((error) => { const dottedPath = error.path?.map((item) => typeof item === "object" ? item.key : item).join("."); if (dottedPath == null || field._path.value == null) return false; const { isPart } = isSubPath({ childPath: dottedPath, parentPath: field._path.value }); if (dottedPath === field._path.value) return true; return isPart; }).map((error) => { const normalizedPath = error.path?.map((item) => typeof item === "object" ? item.key : item); if (normalizedPath == null || field._path.value == null) return { message: error.message, path: null }; const joinedNormalizedPath = normalizedPath.join("."); if (joinedNormalizedPath === field._path.value) return { message: error.message, path: null }; const { isPart, relativePath } = isSubPath({ childPath: joinedNormalizedPath, parentPath: field._path.value }); if (!isPart) return { message: error.message, path: null }; return { message: error.message, path: relativePath }; }); }); return field; } function registerParentPaths(path) { const pathParts = path.split("."); for (let i = pathParts.length - 1; i >= 0; i--) { const part = pathParts[i]; if (!Number.isNaN(Number(part))) register(pathParts.slice(0, i + 1).join(".")); } } const register = (path, defaultValue) => { const existingId = getIdByPath(paths.value, path); const clonedDefaultValue = deepClone(defaultValue); if (existingId !== null) { let field$1 = registeredFields.get(existingId) ?? null; if (field$1 === null) field$1 = createField(existingId, path, get(form.value, path)); if ((field$1.modelValue.value === null || Array.isArray(field$1.modelValue.value) && field$1.modelValue.value.length === 0) && defaultValue !== void 0) field$1.setValue(clonedDefaultValue); return getFieldWithTrackedDependencies(field$1, clonedDefaultValue ?? null); } const value = get(form.value, path); if (value == null) set(form.value, path, clonedDefaultValue ?? null); const id = generateId(); paths.value.set(id, path); const field = createField(id, path, value); registeredFields.set(id, field); registerParentPaths(path); registerFieldWithDevTools(formId, field); return getFieldWithTrackedDependencies(field, clonedDefaultValue ?? null); }; const registerArray = (path, defaultValue) => { const existingId = getIdByPath(paths.value, path); const clonedDefaultValue = deepClone(defaultValue); if (existingId !== null) { let fieldArray$1 = registeredFieldArrays.get(existingId) ?? null; if (fieldArray$1 === null) fieldArray$1 = createFieldArray(existingId, path, get(form.value, path) ?? []); return getFieldWithTrackedDependencies(fieldArray$1, []); } const value = get(form.value, path); if (value == null) set(form.value, path, []); const id = generateId(); paths.value.set(id, path); const fieldArray = createFieldArray(id, path, value ?? []); if (clonedDefaultValue !== void 0 && (value == null || value.length === 0)) { const defaultValueAsArray = clonedDefaultValue; for (const value$1 of defaultValueAsArray) fieldArray.append(value$1); } registeredFieldArrays.set(id, fieldArray); registerParentPaths(path); return getFieldWithTrackedDependencies(fieldArray, []); }; const unregister = (path) => { const id = getIdByPath(paths.value, path); unset(form.value, path); if (id === null) return; updatePaths(path); registeredFields.delete(id); trackedDependencies.delete(id); paths.value.delete(id); unregisterFieldWithDevTools(id); }; function blurAll() { for (const field of registeredFields.values()) field.onBlur(); } async function submit() { hasAttemptedToSubmit.value = true; blurAll(); if (!isValid.value) { onSubmitFormErrorCb?.({ data: form.value, errors: formattedErrors.value }); return; } const currentFormState = deepClone(form); isSubmitting.value = true; if (onSubmitCb == null) throw new Error("Attempted to submit form but `onSubmitForm` callback is not registered"); const validatedResult = await schema["~standard"].validate(form.value); if (validatedResult.issues) { onSubmitFormErrorCb?.({ data: form.value, errors: formattedErrors.value }); return; } initialFormState.value = deepClone(currentFormState.value); await onSubmitCb(validatedResult.value); isSubmitting.value = false; } function setValues(values) { for (const path in values) set(form.value, path, values[path]); } function addErrors(err) { const standardErrors = err.map((error) => { if (error.path == null) return { ...error, path: [] }; const newPath = error.path.split("."); return { ...error, path: newPath }; }); rawErrors.value = [...rawErrors.value, ...standardErrors]; } watch(() => form.value, async () => { const result = await schema["~standard"].validate(form.value); if (result.issues != null && result.issues.length > 0) { rawErrors.value = result.issues; return; } rawErrors.value = []; }, { deep: true, immediate: true }); function reset() { if (initialState == null) throw new Error("In order to reset the form, you need to provide an initial state"); Object.assign(form.value, deepClone(toValue(initialState))); for (const [_, field] of registeredFields) field._isTouched.value = false; hasAttemptedToSubmit.value = false; } const formObject = { _id: formId, hasAttemptedToSubmit: computed(() => hasAttemptedToSubmit.value), isDirty: computed(() => isDirty.value), isSubmitting: computed(() => isSubmitting.value), isValid, addErrors, blurAll, errors: computed(() => formattedErrors.value), rawErrors: computed(() => rawErrors.value), register, registerArray, reset, setValues, state: computed(() => form.value), submit, unregister }; registerFormWithDevTools(formObject); return formObject; } //#endregion export { formatErrorsToZodFormattedError, useForm };