UNPKG

@vue-composable-form/core

Version:

409 lines (393 loc) 16.4 kB
import { toRefs, reactive, ref, watch, unref } from 'vue'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } const isFunction = (val) => typeof val === 'function'; 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 isRadioInput = (el) => el.type === 'radio'; const isCheckBoxInput = (el) => el.type === 'checkbox'; const isEmpty = (val) => val === '' || val === null || val === undefined; const isRegex = (val) => val instanceof RegExp; const VALIDATION_MODE = { onBlur: 'onBlur', onChange: 'onChange', onSubmit: 'onSubmit', onTouched: 'onTouched', all: 'all', }; 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 createSubmitHandler(fn) { return fn; } function createErrorHandler(fn) { return fn; } function set(obj, key, value) { obj[key] = value; return obj; } function get(obj, key) { return obj[key]; } function unset(obj, key) { delete obj[key]; return obj; } function getValueAndMessage(field) { if (isUndefined(field)) return { value: undefined, message: '' }; if (isString(field)) return { value: true, message: field }; if (isBoolean(field)) return { value: field, message: '' }; return field; } function getValidatorError(result, ref, type = 'validate') { if ((isBoolean(result) && !result)) { return { type: type, message: isString(result) ? result : '', ref, }; } } function handleDeferError(error, shouldError) { if (!isEmptyObject(error) && shouldError) { error.ref.focus(); } } function validateField(field, validateAllFieldCriteria, shouldFocusOnError) { return __awaiter(this, void 0, void 0, function* () { const { inputValue } = field; const { required, min, max, maxLength, minLength, pattern, validate, valueAsNumber, valueAsDate, setValueAs, shouldUnregister, onChange, onBlur, disabled, } = field.rule; const el = field.ref; const isRadio = isRadioInput(el); const isCheckBox = isCheckBoxInput(el); const isRadioOrCheckBox = isRadio || isCheckBox; const isEmptyValue = isEmpty(inputValue); let error = {}; try { if (required && !isRadioOrCheckBox) { const { value, message } = getValueAndMessage(required); if (isEmptyValue && value) { error = { type: 'required', message, ref: el, }; 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(inputValue)) { const inputNumber = el.valueAsNumber || +inputValue; if (minValue && inputNumber < minValue) exceedMin = true; if (maxValue && inputNumber > maxValue) exceedMax = true; } else { if (isString(inputValue)) { const valueDate = el.valueAsDate || new Date(inputValue); if (minValue && valueDate < minValue) exceedMin = true; if (maxValue && valueDate > maxValue) exceedMax = true; } } if (exceedMax || exceedMin) { error = { type: exceedMax ? 'max' : 'min', message: exceedMax ? maxMsg : minMsg, ref: el, }; 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, ref: el, }; return error; } } if (pattern && !isEmptyValue && isString(inputValue)) { const { value: patternValue, message } = getValueAndMessage(pattern); if (isRegex(patternValue) && !inputValue.match(patternValue)) { error = { type: 'pattern', message, ref: el, }; if (!validateAllFieldCriteria) return error; } } if (validate) { if (isFunction(validate)) { const result = yield validate(inputValue); const validateResult = getValidatorError(result, el); if (validateResult) error = validateResult; if (!validateAllFieldCriteria) return error; } else if (isObject(validate)) { for (const key in validate) { const result = yield validate[key](inputValue); const validateResult = getValidatorError(result, el, key); if (validateResult) error = validateResult; if (!validateAllFieldCriteria) return error; } } } } finally { handleDeferError(error, shouldFocusOnError); } return error; }); } const onModelValueUpdate = 'onUpdate:modelValue'; function createForm(_options) { // what about use Map? const fields = reactive({}); const formState = reactive({ isDirty: false, isValidating: false, dirtyFields: {}, isSubmitted: false, submitCount: 0, isSubmitting: false, isSubmitSuccessful: false, isValid: false, errors: {}, }); const validationModeBeforeSubmit = getValidationMode(_options.mode); getValidationMode(_options.reValidateMode); const shouldDisplayAllAssociatedErrors = _options.criteriaMode === VALIDATION_MODE.all; const _getDefaultVal = (name) => { return get(_options.defaultValues, name) || ''; }; const _transformRef = (ref) => { var _a; const unwrap = unref(ref); let el; if (isHTMLElement(unwrap)) el = unwrap; else if (isHTMLElement(unwrap === null || unwrap === void 0 ? void 0 : unwrap.$el)) el = unwrap.$el; else if (isHTMLElement((_a = unwrap === null || unwrap === void 0 ? void 0 : unwrap.ref) === null || _a === void 0 ? void 0 : _a.value)) el = unwrap.ref.value; if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') return el; return el.querySelectorAll('input, select, textarea')[0]; }; const _validateFieldByName = (fieldName, isValidateAllFields = false) => __awaiter(this, void 0, void 0, function* () { // eslint-disable-next-line @typescript-eslint/no-use-before-define cleanupUnRegister(); if (isEmptyObject(fields) || isNullOrUndefined(fields[fieldName])) { return; } !isValidateAllFields && set(formState, 'isValidating', true); const res = yield validateField(fields[fieldName], shouldDisplayAllAssociatedErrors, unref(_options.shouldFocusError)); !isValidateAllFields && set(formState, 'isValidating', false); if (Object.keys(res).length) { formState.errors[fieldName] = res; } else { unset(formState.errors, fieldName); } }); const _validateFields = () => __awaiter(this, void 0, void 0, function* () { for (const fieldName of Object.keys(fields)) { set(formState, 'isValidating', true); yield _validateFieldByName(fieldName, true); set(formState, 'isValidating', false); } }); const _handleDirtyFields = (name, evt) => { const inputVal = isString(evt) ? evt : evt.target.value; const defaultVal = _getDefaultVal(name); if (defaultVal === inputVal) { set(formState, 'isDirty', false); unset(formState.dirtyFields, name); } else { set(formState, 'isDirty', true); set(formState.dirtyFields, name, true); } }; const onChange = (name) => __awaiter(this, void 0, void 0, function* () { set(formState, 'isValidating', true); yield _validateFieldByName(name); set(formState, 'isValidating', false); if (isEmptyObject(formState.errors)) { set(formState, 'isValid', true); } else { set(formState, 'isValid', false); } }); const handleSubmit = (onSubmit, onError) => { set(formState, 'isSubmitting', true); formState.submitCount++; return (e) => __awaiter(this, void 0, void 0, function* () { yield _validateFields(); if (!isEmptyObject(formState.errors)) { if (isFunction(onError)) { yield onError(formState.errors, e); set(formState, 'isSubmitting', false); set(formState, 'isSubmitted', true); } return; } const res = {}; for (const fieldName in fields) { res[fieldName] = fields[fieldName].inputValue; } yield onSubmit(fields, e); set(formState, 'isSubmitting', false); set(formState, 'isSubmitted', true); set(formState, 'isSubmitSuccessful', true); }); }; const createErrorHandler$1 = (fn) => createErrorHandler(fn); const createSubmitHandler$1 = (fn) => createSubmitHandler(fn); const unRegisterSet = new Set(); const register = (name, options) => { var _a; const modelVal = ref(((_a = fields[name]) === null || _a === void 0 ? void 0 : _a.inputValue) || ''); const elRef = ref(null); if (!fields[name]) { set(fields, name, {}); assignBindAttrs(); } if (options.value) { set(fields[name], 'inputValue', options.value); unset(options, 'value'); } watch(elRef, (newEl) => { if (newEl) { const el = _transformRef(elRef); if (isHTMLElement(el)) { if (!isNullOrUndefined(fields[name])) fields[name].ref = el; } } }); function assignBindAttrs(el = {}, newValue = options.value) { elRef.value = el; modelVal.value = modelVal; set(fields, name, { inputValue: newValue, rule: Object.assign({}, options), ref: elRef.value, name: name, }); } return { ref: elRef, modelValue: modelVal.value, onBlur: () => { if (validationModeBeforeSubmit.isOnBlur) onChange(name); }, [onModelValueUpdate]: (newValue) => { assignBindAttrs(_transformRef(elRef), newValue); if (validationModeBeforeSubmit.isOnChange) onChange(name); }, onInput(evt) { _handleDirtyFields(name, evt); // filter UI Component if (isString(evt)) return; assignBindAttrs(_transformRef(elRef), evt.target.value); if (validationModeBeforeSubmit.isOnChange) onChange(name); }, }; }; const unregister = (fieldsName) => { if (!isArray(fieldsName)) { fieldsName = [fieldsName]; } fieldsName.forEach(fieldName => unRegisterSet.add(fieldName)); }; const cleanupUnRegister = () => { for (const fieldName of unRegisterSet) { unset(formState.errors, fieldName); unset(fields, fieldName); unset(formState.dirtyFields, fieldName); } }; const useRegister = (name, options) => () => register(name, options); return { formState: toRefs(formState), register, unregister, useRegister, handleSubmit, createSubmitHandler: createSubmitHandler$1, createErrorHandler: createErrorHandler$1, }; } function useForm(props = {}) { props = Object.assign({ mode: 'onSubmit', reValidateMode: 'onChange', defaultValues: {}, criteriaMode: 'firstError', shouldFocusError: true, shouldUnregister: false, shouldUseNativeValidation: false, delayError: undefined }, props); return Object.assign({}, createForm(props)); } export { useForm };