vue-use-form
Version:
```bash # npm npm i vue-use-form
806 lines (784 loc) • 23.8 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const vue = require('vue');
const VALIDATION_MODE = {
onBlur: "onBlur",
onChange: "onChange",
onSubmit: "onSubmit",
onTouched: "onTouched",
all: "all"
};
function createSubmitHandler(fn) {
return fn;
}
function createErrorHandler(fn) {
return fn;
}
function set(obj, key, value) {
obj[key] = value;
return Reflect.set(obj, key, value);
}
function get(obj, key) {
return Reflect.get(obj, key);
}
function unset(obj, key) {
return Reflect.deleteProperty(obj, key);
}
const hasProp = (obj, key) => Reflect.has(obj, key);
const isFunction = (val) => typeof val === "function";
const isNumber = (val) => typeof val === "number" && !isNaN(val);
const isString = (val) => typeof val === "string";
const isBoolean = (val) => typeof val === "boolean";
const isObject = (val) => val !== null && typeof val === "object";
const isArray = (val) => Array.isArray(val);
const isEmptyObject = (val) => isObject(val) && Object.keys(val).length === 0;
const isUndefined = (val) => typeof val === "undefined";
const isNull = (val) => val === null;
const isNullOrUndefined = (val) => isNull(val) || isUndefined(val);
const isHTMLElement = (val) => val instanceof HTMLElement;
const isEmpty = (val) => val === "" || val === null || val === void 0;
const isRegex = (val) => val instanceof RegExp;
const isObjectType = (val) => typeof val === "object";
const isPrimitive = (val) => isNullOrUndefined(val) || !isObjectType(val);
const isDateObject = (val) => val instanceof Date;
function deepEqual(obj1, obj2) {
if (isPrimitive(obj1) || isPrimitive(obj2)) {
return obj1 === obj2;
}
if (isDateObject(obj1) && isDateObject(obj2)) {
return obj1.getTime() === obj2.getTime();
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false;
}
if (key !== "ref") {
const val1 = obj1[key];
const val2 = obj2[key];
if (isObject(val1) || isObject(val2) || isObject(val1) && isObject(val2) || isDateObject(val1) && isDateObject(val2) ? !deepEqual(val1, val2) : val1 !== val2) {
return false;
}
}
}
return true;
}
const isRadioInput = (el) => el?.type === "radio";
const isCheckBoxInput = (el) => el?.type === "checkbox";
const isRadioOrCheckboxInput = (el) => isRadioInput(el) || isCheckBoxInput(el);
function isFieldElement(el) {
return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
}
const getValidFormElement = (el) => el.querySelectorAll("input, textarea, select")[0];
function getFormEl(elRef) {
elRef = vue.unref(elRef);
if (isFieldElement(elRef)) {
return elRef;
} else if (isHTMLElement(elRef)) {
return getValidFormElement(elRef);
} else if (isObject(elRef)) {
const keys = Reflect.ownKeys(elRef);
for (const key of keys) {
const val = vue.unref(elRef[key]);
if (isFieldElement(val)) {
return val;
} else if (isHTMLElement(val)) {
return val.querySelectorAll("input, textarea, select")[0];
}
}
}
}
function getValidationMode(mode) {
return {
isOnSubmit: !mode || mode === VALIDATION_MODE.onSubmit,
isOnBlur: mode === VALIDATION_MODE.onBlur,
isOnChange: mode === VALIDATION_MODE.onChange,
isOnAll: mode === VALIDATION_MODE.all,
isOnTouch: mode === VALIDATION_MODE.onTouched
};
}
function warn(msg) {
console.warn(`[Vue Use Form] ${msg}`);
}
function getValueAndMessage(field) {
if (isUndefined(field))
return { value: void 0, message: "" };
if (isString(field))
return { value: true, message: field };
if (isBoolean(field))
return { value: field, message: "" };
if (isNumber(field))
return { value: field, message: "" };
if (isDateObject(field)) {
return { value: field, message: "" };
}
return field;
}
function getValidatorError(result, type = "validate") {
if (isBoolean(result) && !result) {
return {
type,
message: isString(result) ? result : ""
};
} else if (isString(result)) {
return {
type,
message: result
};
}
}
const InvalidDate = new Date("foo");
function handleValidateError(error, shouldFocusOnError, el) {
if (!isFieldElement(el)) {
return;
}
if (!isEmptyObject(error) && shouldFocusOnError) {
el.focus();
}
}
async function validateField(field, shouldFocusOnError, validateAllFieldCriteria) {
const inputValue = vue.unref(field.inputValue);
const {
required,
min,
max,
maxLength,
minLength,
pattern,
validate,
valueAsNumber,
valueAsDate,
setValueAs,
disabled = false
} = field.rule;
const el = vue.unref(field.el);
const isRadio = isRadioInput(el);
const isCheckbox = isCheckBoxInput(el);
const isRadioOrCheckBox = isRadio || isCheckbox;
const unrefInputVal = vue.unref(inputValue);
const isEmptyValue = isEmpty(unrefInputVal);
let error = {};
try {
if (isFieldElement(el)) {
if (valueAsNumber) {
const elVal = el.value;
set(field, "inputValue", vue.ref(el.valueAsNumber || elVal === "" ? elVal : parseFloat(elVal)));
} else if (valueAsDate) {
set(field, "inputValue", vue.ref(el.valueAsDate || InvalidDate));
} else if (setValueAs) {
set(field, "inputValue", vue.ref(setValueAs(unrefInputVal)));
}
}
if (required && !isRadioOrCheckBox) {
const { value, message } = getValueAndMessage(required);
if (isEmptyValue && value) {
error = {
type: "required",
message
};
if (!validateAllFieldCriteria)
return error;
}
}
if (!isEmptyValue && (!isNullOrUndefined(max) || !isNullOrUndefined(min))) {
let exceedMax;
let exceedMin;
const { value: maxValue, message: maxMsg } = getValueAndMessage(max);
const { value: minValue, message: minMsg } = getValueAndMessage(min);
if (!isNaN(unrefInputVal)) {
if (minValue && unrefInputVal < minValue)
exceedMin = true;
if (maxValue && unrefInputVal > maxValue)
exceedMax = true;
} else {
if (minValue && unrefInputVal < minValue)
exceedMin = true;
if (maxValue && unrefInputVal > maxValue)
exceedMax = true;
}
if (exceedMax || exceedMin) {
error = {
type: exceedMax ? "max" : "min",
message: exceedMax ? maxMsg : minMsg
};
if (!validateAllFieldCriteria)
return error;
}
}
if ((maxLength || minLength) && !isEmptyValue && isString(inputValue)) {
let exceedMax;
let exceedMin;
const { value: maxValue, message: maxMsg } = getValueAndMessage(maxLength);
const { value: minValue, message: minMsg } = getValueAndMessage(minLength);
if (minValue && inputValue.length <= minValue)
exceedMin = true;
if (maxValue && inputValue.length >= maxValue)
exceedMax = true;
if (exceedMax || exceedMin) {
error = {
type: exceedMax ? "maxLength" : "minLength",
message: exceedMax ? maxMsg : minMsg
};
return error;
}
}
if (pattern && !isEmptyValue && isString(inputValue)) {
const { value: patternValue, message } = getValueAndMessage(pattern);
if (isRegex(patternValue) && !inputValue.match(patternValue)) {
error = {
type: "pattern",
message
};
if (!validateAllFieldCriteria)
return error;
}
}
if (disabled && isFieldElement(el)) {
el.setAttribute("disabled", "");
} else if (!disabled && isFieldElement(el)) {
el.removeAttribute("disabled");
}
if (validate) {
if (isFunction(validate)) {
const result = await validate(unrefInputVal);
const validateResult = getValidatorError(result);
if (validateResult)
error = validateResult;
if (!validateAllFieldCriteria)
return error;
} else if (isObject(validate)) {
for (const key in validate) {
const result = await validate[key](unrefInputVal);
const validateResult = getValidatorError(result, key);
if (validateResult)
error = validateResult;
if (!validateAllFieldCriteria)
return error;
}
}
}
} finally {
handleValidateError(error, shouldFocusOnError, el);
}
return error;
}
function creatFormControl(_options) {
const _fields = {};
const _formState = vue.reactive({
get isDirty() {
if (isEmptyObject(_formState.dirtyFields)) {
return false;
}
return true;
},
isValidating: false,
dirtyFields: {},
isSubmitted: false,
submitCount: 0,
isSubmitting: false,
isSubmitSuccessful: false,
isValid: false,
defaultValues: _options.defaultValues || {},
errors: {}
});
const _defaultValues = _options.defaultValues || {};
const _fieldArrayDefaultValues = {};
const validationModeBeforeSubmit = getValidationMode(_options.mode);
const shouldDisplayAllAssociatedErrors = _options.criteriaMode === VALIDATION_MODE.all;
const _setFormState = (props) => {
Object.entries(props).forEach(([key, val]) => {
set(_formState, key, val);
});
};
const _setFormStateError = (fieldName, error) => {
set(_formState.errors, fieldName, error);
};
const _getFormStateError = (fieldName) => fieldName ? get(_formState.errors, fieldName) : _formState.errors;
const _removeFormStateError = (fieldName) => {
unset(_formState.errors, fieldName);
};
const _setFields = (name, fieldOptions) => {
const field = get(_fields, name);
if (isNullOrUndefined(field)) {
set(_fields, name, {});
}
set(_fields, name, { ...field, ...fieldOptions });
};
const _setValidating = (isValidating) => _setFormState({ isValidating });
const _getFieldProp = (name, prop) => {
return get(_fields[name], prop);
};
const _getFieldDom = (name) => {
return _getFieldProp(name, "el");
};
const _getDirtyFields = () => {
const dirtyFields = {};
Object.entries(_fields).forEach(([key, val]) => {
if (val.isDirty) {
set(dirtyFields, key, true);
}
});
return dirtyFields;
};
const _handleDirtyField = (fieldName) => {
if (_fields[fieldName].isUnregistered) {
return;
}
const defaultVal = get(_defaultValues, fieldName);
const val = _fields[fieldName].inputValue.value;
if (deepEqual(defaultVal, val)) {
_setFields(fieldName, {
isDirty: false
});
} else {
_setFields(fieldName, {
isDirty: true
});
}
};
const _handleAllDirtyFieldsOperate = (fieldNames) => {
if (isUndefined(fieldNames)) {
Object.keys(_fields).forEach((fieldName) => {
_handleDirtyField(fieldName);
});
} else if (!isArray(fieldNames)) {
fieldNames = [fieldNames];
fieldNames.forEach((fieldName) => {
_handleDirtyField(fieldName);
});
}
_setFormState({
dirtyFields: _getDirtyFields()
});
};
const _handleIsValidFields = () => {
_setFormState({
isValid: isEmptyObject(_formState.errors)
});
};
const _validate = async (fieldName, isValidateAllFields = false) => {
const field = _fields[fieldName];
if (isEmptyObject(_fields) || isNullOrUndefined(field)) {
return;
}
if (field.rule.disabled) {
field.inputValue.value = "";
return;
}
const setValidating = (payload) => !isValidateAllFields && _setValidating(payload);
const resolver = _options.resolver;
setValidating(true);
let res = {};
if (isFunction(resolver)) {
const values = Object.fromEntries(Object.entries(_fields).map(([key, val]) => [key, val.inputValue.value]));
const errors = await resolver(values);
if (!isEmptyObject(errors)) {
res = errors[fieldName];
}
} else {
res = await validateField(field, vue.unref(_options.shouldFocusError), shouldDisplayAllAssociatedErrors);
}
if (isFunction(resolver) && isEmptyObject(res) && !isEmptyObject(_fields[fieldName].rule)) {
res = await validateField(field, vue.unref(_options.shouldFocusError), shouldDisplayAllAssociatedErrors);
}
setValidating(false);
if (_options.delayError && _options.delayError > 0) {
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, _options.delayError);
});
}
if (Object.keys(res || {}).length) {
_setFormStateError(fieldName, res);
} else {
_removeFormStateError(fieldName);
}
};
const _validateAllFields = async () => {
_setValidating(true);
for (const fieldName of Object.keys(_fields)) {
await _validate(fieldName, true);
}
_setValidating(false);
};
const trigger = async (name) => {
await (isString(name) ? _validate(name) : _validateAllFields());
};
const _onChange = async (name) => {
await trigger(name);
_handleIsValidFields();
};
const triggerValidate = async (fieldNames) => {
if (isUndefined(fieldNames)) {
await _validateAllFields();
} else {
if (!isArray(fieldNames)) {
fieldNames = [fieldNames];
}
await Promise.all(fieldNames.map((name) => _validate(name)));
}
};
const reset = (values, keepStateOptions) => {
if (!keepStateOptions) {
keepStateOptions = {};
}
if (!values) {
values = {};
}
const setFormState = () => {
const dirtyFields = _getDirtyFields();
_setFormState({
isSubmitted: keepStateOptions.keepIsSubmitted ? _formState.isSubmitted : false,
submitCount: keepStateOptions.keepSubmitCount ? _formState.submitCount : 0,
errors: keepStateOptions.keepErrors ? _formState.errors : {},
isDirty: keepStateOptions.keepDirty ? _formState.isDirty : !isEmptyObject(dirtyFields),
dirtyFields: keepStateOptions.keepDirty ? _formState.dirtyFields : dirtyFields,
isSubmitting: false,
isSubmitSuccessful: false,
isValid: keepStateOptions.keepIsValid ? _formState.isValid : false
});
};
Object.entries(values).forEach(([key, val]) => {
_fields[key].inputValue.value = val;
});
setFormState();
};
const handleSubmit = (onSubmit, onError) => {
_setFormState({
isSubmitting: true
});
_formState.submitCount++;
return async (e) => {
await _onChange();
_handleAllDirtyFieldsOperate();
if (!isEmptyObject(_formState.errors)) {
if (isFunction(onError)) {
await onError(_formState.errors, e);
_setFormState({
isSubmitting: false,
isSubmitted: true
});
}
return;
}
for (const fieldName in _fields) {
_fields[fieldName].inputValue;
}
await onSubmit(_fields, e);
_setFormState({
isSubmitting: false,
isSubmitted: true,
isSubmitSuccessful: true
});
};
};
const createErrorHandler$1 = (fn) => createErrorHandler(fn);
const createSubmitHandler$1 = (fn) => createSubmitHandler(fn);
const setError = (fieldName, error, config) => {
if (!config) {
config = {
shouldFocusError: true
};
}
_setFormStateError(fieldName, error);
if (config.shouldFocusError) {
handleValidateError(error, true, _getFieldDom(fieldName));
}
};
const clearErrors = (fieldName) => {
if (isUndefined(fieldName)) {
set(_formState, "errors", {});
} else {
if (!isArray(fieldName)) {
fieldName = [fieldName];
}
fieldName.forEach((name) => {
_removeFormStateError(name);
});
}
};
const _setFieldsValue = (name, value) => {
_fields[name].inputValue.value = value;
};
const setValue = async (name, value, config = {}) => {
if (isNullOrUndefined(_fields[name])) {
warn(`setValue cannot set not exist field #${name}`);
return;
}
config = {
shouldValidate: true,
shouldDirty: true,
...config
};
_setFieldsValue(name, value);
if (config.shouldDirty) {
_handleAllDirtyFieldsOperate(name);
}
if (config.shouldValidate) {
await trigger(name);
}
};
const setFocus = (name) => vue.nextTick(() => {
const el = _getFieldDom(name);
if (el) {
el.focus();
}
});
const getValues = (fieldNames) => {
const res = {};
if (isUndefined(fieldNames)) {
Object.entries(_fields).forEach(([name, field]) => {
set(res, name, field.inputValue.value);
});
} else {
if (!isArray(fieldNames)) {
fieldNames = [fieldNames];
}
fieldNames.forEach((name) => {
set(res, name, _fields[name].inputValue.value);
});
}
return res;
};
const getFieldState = (fieldName) => {
return {
isValid: !isEmptyObject(_getFormStateError(fieldName)),
isDirty: !isUndefined(_formState.dirtyFields[fieldName]),
error: _getFormStateError(fieldName) || {}
};
};
const register = (fieldName, options) => {
if (isUndefined(options)) {
options = {};
}
let isModelValue = false;
let field = get(_fields, fieldName);
const {
vModelBinding = "modelValue"
} = options;
const defaultVal = options?.value || get(_defaultValues, fieldName) || get(_fieldArrayDefaultValues, fieldName.split(".").find((item) => isNumber(parseInt(item)))) || "";
if (!field) {
_setFields(fieldName, {
inputValue: vue.ref(defaultVal),
rule: options,
isDirty: false,
isUnregistered: false,
el: vue.ref(null)
});
field = get(_fields, fieldName);
}
const addEventListenerToElement = () => {
if (isFieldElement(field.el) || _fields[fieldName].isUnregistered) {
return;
}
const el = getFormEl(field.el);
_setFields(fieldName, { ..._fields[fieldName], el });
if (isRadioOrCheckboxInput(el)) {
set(_defaultValues, fieldName, !!defaultVal);
}
set(_defaultValues, fieldName, defaultVal);
if (isFieldElement(el)) {
if (validationModeBeforeSubmit.isOnBlur) {
el.addEventListener("blur", async () => {
await _onChange(fieldName);
});
} else if (validationModeBeforeSubmit.isOnTouch) {
el.addEventListener("click", async () => {
await _onChange(fieldName);
});
}
}
};
const handleValueChange = async (input) => {
field.inputValue.value = input?.target?.value || input || "";
_handleAllDirtyFieldsOperate(fieldName);
if (validationModeBeforeSubmit.isOnChange) {
await _onChange(fieldName);
}
};
return {
...!isFieldElement(field.el) && { ref: _fields[fieldName].el },
...vModelBinding === "modelValue" && {
value: field.inputValue.value,
onInput: (e) => {
if (_fields[fieldName].isUnregistered) {
return;
}
addEventListenerToElement();
queueMicrotask(async () => {
if (!isModelValue) {
await handleValueChange(e.target?.value);
}
});
}
},
[vModelBinding]: field.inputValue.value,
[`onUpdate:${vModelBinding}`]: (input) => {
if (_fields[fieldName].isUnregistered) {
return;
}
isModelValue = true;
addEventListenerToElement();
handleValueChange(input);
}
};
};
const unregister = (fieldName, options = {}) => {
if (isNullOrUndefined(_fields[fieldName])) {
warn(`cannot unregister not exist field #${fieldName}`);
return;
}
options = {
keepDirty: false,
keepError: false,
keepValue: false,
...options
};
if (!options.keepDirty) {
unset(_formState.dirtyFields, fieldName);
}
if (!options.keepError) {
unset(_formState.errors, fieldName);
}
if (!options.keepValue) {
_setFieldsValue(fieldName, _defaultValues[fieldName] || "");
}
set(_fields[fieldName], "isUnregistered", true);
};
const isExistInErrors = (fieldName) => Object.keys(_formState.errors).includes(fieldName);
return vue.reactive({
control: {
_fields,
_formState,
_fieldArrayDefaultValues,
handleSubmit,
createErrorHandler: createErrorHandler$1,
createSubmitHandler: createSubmitHandler$1,
register,
unregister,
reset,
setError,
clearErrors,
setValue,
setFocus,
getValues,
getFieldState,
triggerValidate
},
formState: _formState,
handleSubmit,
createErrorHandler: createErrorHandler$1,
createSubmitHandler: createSubmitHandler$1,
register,
unregister,
reset,
setError,
clearErrors,
setValue,
setFocus,
getValues,
getFieldState,
triggerValidate,
isExistInErrors
});
}
function useForm(props = {}) {
props = {
mode: "onSubmit",
reValidateMode: "onChange",
defaultValues: {},
criteriaMode: "firstError",
shouldFocusError: true,
delayError: 0,
...props
};
return {
...creatFormControl(props)
};
}
function createFieldArray(_options) {
const { name, control } = _options;
const _fields = vue.reactive([]);
let fieldIndex = 0;
const _createFields = (fieldName, defaultVal) => {
let index = fieldIndex;
fieldIndex++;
set(control._fieldArrayDefaultValues, index, defaultVal);
return {
index,
name: fieldName
};
};
const append = (fields) => {
Object.entries(fields).forEach(([fieldName, defaultVal]) => {
_fields.push(_createFields(fieldName, defaultVal));
});
};
const prepend = (fields) => {
Object.entries(fields).forEach(([fieldName, defaultVal]) => {
_fields.unshift(_createFields(fieldName, defaultVal));
});
};
const remove = (indexes) => {
if (!isArray(indexes)) {
indexes = [indexes];
}
for (const index of indexes) {
const targetIndex = _fields.findIndex((field) => field.index === index);
if (targetIndex >= 0) {
_fields.splice(targetIndex, 1);
}
}
};
const insert = (startIndex, fields) => {
const fieldsMap = Object.entries(fields).map(([fieldName, options]) => _createFields(fieldName, options));
_fields.splice(startIndex, 0, ...fieldsMap);
};
const swap = (from, to) => {
const temp = _fields[from];
_fields[from] = _fields[to];
_fields[to] = temp;
};
return {
append,
prepend,
remove,
insert,
swap,
fields: _fields
};
}
function useFieldArray(props) {
return {
...createFieldArray(props)
};
}
exports.createErrorHandler = createErrorHandler;
exports.createSubmitHandler = createSubmitHandler;
exports.get = get;
exports.hasProp = hasProp;
exports.isArray = isArray;
exports.isBoolean = isBoolean;
exports.isDateObject = isDateObject;
exports.isEmpty = isEmpty;
exports.isEmptyObject = isEmptyObject;
exports.isFunction = isFunction;
exports.isHTMLElement = isHTMLElement;
exports.isNull = isNull;
exports.isNullOrUndefined = isNullOrUndefined;
exports.isNumber = isNumber;
exports.isObject = isObject;
exports.isObjectType = isObjectType;
exports.isPrimitive = isPrimitive;
exports.isRegex = isRegex;
exports.isString = isString;
exports.isUndefined = isUndefined;
exports.set = set;
exports.unset = unset;
exports.useFieldArray = useFieldArray;
exports.useForm = useForm;