UNPKG

vue-use-form

Version:

```bash # npm npm i vue-use-form

806 lines (784 loc) 23.8 kB
'use strict'; 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;