formango
Version:
```bash pnpm i formango ```
854 lines (848 loc) • 27.1 kB
JavaScript
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 };