el-form-react
Version:
React form components and hooks powered by Zod validation
395 lines (392 loc) • 11.1 kB
JavaScript
// src/hooks.ts
export * from "el-form-core";
// src/useForm.ts
import { useState, useCallback, useRef } from "react";
import { z } from "zod";
import {
parseZodErrors as parseZodErrors2,
setNestedValue as setNestedValue2,
getNestedValue as getNestedValue2,
removeArrayItem as removeArrayItem2
} from "el-form-core";
// src/utils/index.ts
import {
setNestedValue,
getNestedValue,
removeArrayItem,
parseZodErrors
} from "el-form-core";
function addArrayItemReact(obj, path, item) {
const result = { ...obj };
const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
const keys = normalizedPath.split(".").filter((key) => key !== "");
let current = result;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!isNaN(Number(key))) {
if (Array.isArray(current)) {
current[Number(key)] = Array.isArray(current[Number(key)]) ? [...current[Number(key)]] : { ...current[Number(key)] };
current = current[Number(key)];
}
} else {
if (typeof current[key] !== "object" || current[key] === null) {
current[key] = {};
} else {
current[key] = Array.isArray(current[key]) ? [...current[key]] : { ...current[key] };
}
current = current[key];
}
}
const arrayKey = keys[keys.length - 1];
if (!isNaN(Number(arrayKey))) {
if (Array.isArray(current)) {
current = [...current];
current[Number(arrayKey)] = item;
}
} else {
if (!Array.isArray(current[arrayKey])) {
current[arrayKey] = [];
} else {
current[arrayKey] = [...current[arrayKey]];
}
current[arrayKey].push(item);
}
return result;
}
// src/useForm.ts
function useForm(options) {
const {
schema,
initialValues = {},
validateOnChange = false,
validateOnBlur = false
} = options;
const fieldRefs = useRef(/* @__PURE__ */ new Map());
const [formState, setFormState] = useState({
values: initialValues,
errors: {},
touched: {},
isSubmitting: false,
isValid: false,
isDirty: false
});
const validate = useCallback(
(values) => {
try {
schema.parse(values);
return { isValid: true, errors: {} };
} catch (error) {
if (error instanceof z.ZodError) {
return {
isValid: false,
errors: parseZodErrors2(error)
};
}
return { isValid: false, errors: {} };
}
},
[schema]
);
const checkIsDirty = useCallback(
(currentValues) => {
return JSON.stringify(initialValues || {}) !== JSON.stringify(currentValues || {});
},
[initialValues]
);
const checkFieldIsDirty = useCallback(
(fieldName) => {
const initialValue = initialValues[fieldName];
const currentValue = formState.values[fieldName];
return JSON.stringify(initialValue) !== JSON.stringify(currentValue);
},
[initialValues, formState.values]
);
const register = useCallback(
(name) => {
const fieldValue = name.includes(".") ? getNestedValue2(formState.values, name) : formState.values[name];
const isCheckbox = typeof fieldValue === "boolean";
const baseProps = {
name,
onChange: (e) => {
const target = e.target;
const value = target.type === "checkbox" ? target.checked : target.type === "number" ? target.value ? Number(target.value) : void 0 : target.value;
setFormState((prev) => {
const newValues = name.includes(".") ? setNestedValue2(prev.values, name, value) : { ...prev.values, [name]: value };
let newErrors = { ...prev.errors };
if (name.includes(".")) {
const nestedError = getNestedValue2(newErrors, name);
if (nestedError) {
newErrors = setNestedValue2(newErrors, name, void 0);
}
} else {
delete newErrors[name];
}
if (validateOnChange) {
const { errors } = validate(newValues);
newErrors = errors;
}
return {
...prev,
values: newValues,
errors: newErrors,
isDirty: checkIsDirty(newValues)
};
});
},
onBlur: (_e) => {
setFormState((prev) => {
const newTouched = name.includes(".") ? setNestedValue2(prev.touched, name, true) : { ...prev.touched, [name]: true };
let newErrors = prev.errors;
if (validateOnBlur) {
const { errors } = validate(prev.values);
newErrors = errors;
}
return {
...prev,
touched: newTouched,
errors: newErrors,
isDirty: checkIsDirty(prev.values)
};
});
}
};
if (isCheckbox) {
return {
...baseProps,
checked: Boolean(fieldValue)
};
}
return {
...baseProps,
value: fieldValue || ""
};
},
[formState.values, validateOnChange, validateOnBlur, validate, checkIsDirty]
);
const handleSubmit = useCallback(
(onValid, onError) => {
return (e) => {
e.preventDefault();
setFormState((prev) => ({ ...prev, isSubmitting: true }));
const { isValid, errors } = validate(formState.values);
setFormState((prev) => ({
...prev,
errors,
isValid,
isSubmitting: false,
isDirty: checkIsDirty(formState.values)
}));
if (isValid) {
onValid(formState.values);
} else {
if (onError) {
onError(errors);
}
}
};
},
[formState.values, validate, checkIsDirty]
);
const reset = useCallback(
(options2) => {
const newValues = options2?.values ?? initialValues;
setFormState({
values: newValues,
errors: options2?.keepErrors ? formState.errors : {},
touched: options2?.keepTouched ? formState.touched : {},
isSubmitting: false,
isValid: false,
isDirty: options2?.keepDirty ? formState.isDirty : false
});
},
[initialValues, formState]
);
const setValue = useCallback(
(path, value) => {
setFormState((prev) => {
const newValues = setNestedValue2(prev.values, path, value);
const { errors } = validate(newValues);
return {
...prev,
values: newValues,
errors,
isDirty: checkIsDirty(newValues)
};
});
},
[validate, checkIsDirty]
);
const addArrayItemHandler = useCallback(
(path, item) => {
setFormState((prev) => {
const newValues = addArrayItemReact(prev.values, path, item);
const { errors } = validate(newValues);
return {
...prev,
values: newValues,
errors,
isDirty: checkIsDirty(newValues)
};
});
},
[validate, checkIsDirty]
);
const removeArrayItemHandler = useCallback(
(path, index) => {
setFormState((prev) => {
const newValues = removeArrayItem2(prev.values, path, index);
const { errors } = validate(newValues);
return {
...prev,
values: newValues,
errors,
isDirty: checkIsDirty(newValues)
};
});
},
[validate, checkIsDirty]
);
const watch = useCallback(
(nameOrNames) => {
if (!nameOrNames) {
return formState.values;
}
if (Array.isArray(nameOrNames)) {
const result = {};
nameOrNames.forEach((name) => {
result[name] = formState.values[name];
});
return result;
}
return formState.values[nameOrNames];
},
[formState.values]
);
const isDirty = useCallback(
(name) => {
if (name) {
return checkFieldIsDirty(name);
}
return formState.isDirty;
},
[formState.isDirty, checkFieldIsDirty]
);
const getFieldState = useCallback(
(name) => {
return {
isDirty: checkFieldIsDirty(name),
isTouched: !!formState.touched[name],
error: formState.errors[name]
};
},
[formState, checkFieldIsDirty]
);
const getDirtyFields = useCallback(() => {
const dirtyFields = {};
Object.keys(formState.values).forEach((key) => {
const fieldName = key;
if (checkFieldIsDirty(fieldName)) {
dirtyFields[fieldName] = true;
}
});
return dirtyFields;
}, [formState.values, checkFieldIsDirty]);
const getTouchedFields = useCallback(() => {
return { ...formState.touched };
}, [formState.touched]);
const trigger = useCallback(
async (nameOrNames) => {
if (!nameOrNames) {
const { isValid: isValid2 } = validate(formState.values);
return isValid2;
}
if (Array.isArray(nameOrNames)) {
const fieldsToValidate = {};
nameOrNames.forEach((name) => {
fieldsToValidate[name] = formState.values[name];
});
const { isValid: isValid2 } = validate(fieldsToValidate);
return isValid2;
}
const fieldToValidate = {};
fieldToValidate[nameOrNames] = formState.values[nameOrNames];
const { isValid } = validate(fieldToValidate);
return isValid;
},
[formState.values, validate]
);
const clearErrors = useCallback((name) => {
setFormState((prev) => {
if (name) {
const newErrors = { ...prev.errors };
delete newErrors[name];
return { ...prev, errors: newErrors };
}
return { ...prev, errors: {} };
});
}, []);
const setError = useCallback(
(name, error) => {
setFormState((prev) => ({
...prev,
errors: { ...prev.errors, [name]: error }
}));
},
[]
);
const setFocus = useCallback(
(name, options2) => {
const fieldRef = fieldRefs.current.get(name);
if (fieldRef) {
fieldRef.focus();
if (options2?.shouldSelect && "select" in fieldRef) {
fieldRef.select();
}
}
},
[]
);
const resetField = useCallback(
(name) => {
setFormState((prev) => {
const newValues = { ...prev.values };
newValues[name] = initialValues[name];
const newErrors = { ...prev.errors };
delete newErrors[name];
const newTouched = { ...prev.touched };
delete newTouched[name];
return {
...prev,
values: newValues,
errors: newErrors,
touched: newTouched,
isDirty: checkIsDirty(newValues)
};
});
},
[initialValues, checkIsDirty]
);
return {
register,
handleSubmit,
formState,
reset,
setValue,
watch,
getFieldState,
isDirty,
getDirtyFields,
getTouchedFields,
trigger,
clearErrors,
setError,
setFocus,
addArrayItem: addArrayItemHandler,
removeArrayItem: removeArrayItemHandler,
resetField
};
}
export {
useForm
};
//# sourceMappingURL=hooks.mjs.map