UNPKG

@vorms/core

Version:

Vue Form Validation with Composition API

1,037 lines (963 loc) 34.4 kB
import { inject, getCurrentInstance, reactive, ref, computed, provide, onMounted, unref } from 'vue'; var isMergeableObject = function isMergeableObject(value) { return isNonNullObject(value) && !isSpecial(value) }; function isNonNullObject(value) { return !!value && typeof value === 'object' } function isSpecial(value) { var stringValue = Object.prototype.toString.call(value); return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isReactElement(value) } // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 var canUseSymbol = typeof Symbol === 'function' && Symbol.for; var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; function isReactElement(value) { return value.$$typeof === REACT_ELEMENT_TYPE } function emptyTarget(val) { return Array.isArray(val) ? [] : {} } function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) : value } function defaultArrayMerge(target, source, options) { return target.concat(source).map(function(element) { return cloneUnlessOtherwiseSpecified(element, options) }) } function getMergeFunction(key, options) { if (!options.customMerge) { return deepmerge } var customMerge = options.customMerge(key); return typeof customMerge === 'function' ? customMerge : deepmerge } function getEnumerableOwnPropertySymbols(target) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) { return Object.propertyIsEnumerable.call(target, symbol) }) : [] } function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) } function propertyIsOnObject(object, property) { try { return property in object } catch(_) { return false } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target, key) { return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. } function mergeObject(target, source, options) { var destination = {}; if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); }); } getKeys(source).forEach(function(key) { if (propertyIsUnsafe(target, key)) { return } if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { destination[key] = getMergeFunction(key, options)(target[key], source[key], options); } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); } }); return destination } function deepmerge(target, source, options) { options = options || {}; options.arrayMerge = options.arrayMerge || defaultArrayMerge; options.isMergeableObject = options.isMergeableObject || isMergeableObject; // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; var sourceIsArray = Array.isArray(source); var targetIsArray = Array.isArray(target); var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) } } deepmerge.all = function deepmergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } return array.reduce(function(prev, next) { return deepmerge(prev, next, options) }, {}) }; var deepmerge_1 = deepmerge; var cjs = deepmerge_1; var es6 = function equal(a, b) { if (a === b) return true; if (a && b && typeof a == 'object' && typeof b == 'object') { if (a.constructor !== b.constructor) return false; var length, i, keys; if (Array.isArray(a)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false; return true; } if ((a instanceof Map) && (b instanceof Map)) { if (a.size !== b.size) return false; for (i of a.entries()) if (!b.has(i[0])) return false; for (i of a.entries()) if (!equal(i[1], b.get(i[0]))) return false; return true; } if ((a instanceof Set) && (b instanceof Set)) { if (a.size !== b.size) return false; for (i of a.entries()) if (!b.has(i[0])) return false; return true; } if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0;) if (a[i] !== b[i]) return false; return true; } if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); keys = Object.keys(a); length = keys.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0;) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = length; i-- !== 0;) { var key = keys[i]; if (!equal(a[key], b[key])) return false; } return true; } // true if both NaN, false otherwise return a!==a && b!==b; }; function set$1(obj, key, val) { if (typeof val.value === 'object') val.value = klona(val.value); if (!val.enumerable || val.get || val.set || !val.configurable || !val.writable || key === '__proto__') { Object.defineProperty(obj, key, val); } else obj[key] = val.value; } function klona(x) { if (typeof x !== 'object') return x; var i=0, k, list, tmp, str=Object.prototype.toString.call(x); if (str === '[object Object]') { tmp = Object.create(x.__proto__ || null); } else if (str === '[object Array]') { tmp = Array(x.length); } else if (str === '[object Set]') { tmp = new Set; x.forEach(function (val) { tmp.add(klona(val)); }); } else if (str === '[object Map]') { tmp = new Map; x.forEach(function (val, key) { tmp.set(klona(key), klona(val)); }); } else if (str === '[object Date]') { tmp = new Date(+x); } else if (str === '[object RegExp]') { tmp = new RegExp(x.source, x.flags); } else if (str === '[object DataView]') { tmp = new x.constructor( klona(x.buffer) ); } else if (str === '[object ArrayBuffer]') { tmp = x.slice(0); } else if (str.slice(-6) === 'Array]') { // ArrayBuffer.isView(x) // ~> `new` bcuz `Buffer.slice` => ref tmp = new x.constructor(x); } if (tmp) { for (list=Object.getOwnPropertySymbols(x); i < list.length; i++) { set$1(tmp, list[i], Object.getOwnPropertyDescriptor(x, list[i])); } for (i=0, list=Object.getOwnPropertyNames(x); i < list.length; i++) { if (Object.hasOwnProperty.call(tmp, k=list[i]) && tmp[k] === x[k]) continue; set$1(tmp, k, Object.getOwnPropertyDescriptor(x, k)); } } return tmp || x; } const FormContextKey = Symbol((process.env.NODE_ENV !== 'production') ? 'vorms context' : ''); /** * Custom composition API that return form context * * @returns methods and state of form (As same as useForm). {@link FormContextValues} * * @example * ```vue * <script setup lang="ts"> * import { useFormContext } from '@vorms/core' * * const { validateField } = useFormContext() * const { value, attrs } = useField('drink') // You can also use `register` return from `useFormContext()` * </script> * * <template> * <input v-model="value" type="text" v-bind="attrs" /> * </template> * ``` */ function useFormContext() { const context = inject(FormContextKey); return context; } function useFormStore(reducer, initialState) { const state = initialState; const dispatch = (action) => { reducer(state, action); }; return [state, dispatch]; } function injectMaybeSelf(key, defaultValue = undefined) { const vm = getCurrentInstance(); return (vm === null || vm === void 0 ? void 0 : vm.provides[key]) || inject(key, defaultValue); } const InternalContextKey = Symbol((process.env.NODE_ENV !== 'production') ? 'vorms internal context' : ''); function useInternalContext() { const context = injectMaybeSelf(InternalContextKey); return context; } var compact = (value) => Array.isArray(value) ? value.filter(Boolean) : []; var isNullOrUndefined = (value) => value == null; var isDateObject = (value) => value instanceof Date; const isObjectType = (value) => typeof value === 'object'; var isObject = (value) => !isNullOrUndefined(value) && !Array.isArray(value) && isObjectType(value) && !isDateObject(value); var isUndefined = (val) => val === undefined; var get = (obj, path, defaultValue) => { if (!path || !isObject(obj)) { return defaultValue; } const result = compact(path.split(/[,[\].]+?/)).reduce((result, key) => isNullOrUndefined(result) ? result : result[key], obj); return isUndefined(result) || result === obj ? isUndefined(obj[path]) ? defaultValue : obj[path] : result; }; var isFunction = (value) => typeof value === 'function'; var isPromise = (value) => { return !!value && isObject(value) && isFunction(value.then); }; var isString = (value) => typeof value === 'string'; var keysOf = (record) => { return Object.keys(record); }; var isKey = (value) => /^\w*$/.test(value); var stringToPath = (input) => compact(input.replace(/["|']|\]/g, '').split(/\.|\[/)); var set = (obj, path, value) => { let index = -1; const tempPath = isKey(path) ? [path] : stringToPath(path); const length = tempPath.length; const lastIndex = length - 1; while (++index < length) { const key = tempPath[index]; let newValue = value; if (index !== lastIndex) { const objValue = obj[key]; newValue = isObject(objValue) || Array.isArray(objValue) ? objValue : !isNaN(+tempPath[index + 1]) ? [] : {}; } obj[key] = newValue; obj = obj[key]; } return obj; }; function reducer(state, message) { switch (message.type) { case 0 /* ACTION_TYPE.SUBMIT_ATTEMPT */: state.isSubmitting.value = true; state.submitCount.value = state.submitCount.value + 1; return; case 1 /* ACTION_TYPE.SUBMIT_SUCCESS */: state.isSubmitting.value = false; return; case 2 /* ACTION_TYPE.SUBMIT_FAILURE */: state.isSubmitting.value = false; return; case 3 /* ACTION_TYPE.SET_VALUES */: keysOf(state.values).forEach((key) => { delete state.values[key]; }); keysOf(message.payload).forEach((path) => { state.values[path] = message.payload[path]; }); return; case 4 /* ACTION_TYPE.SET_FIELD_VALUE */: set(state.values, message.payload.path, klona(message.payload.value)); return; case 5 /* ACTION_TYPE.SET_TOUCHED */: set(state.touched.value, message.payload.path, message.payload.touched); return; case 6 /* ACTION_TYPE.SET_ERRORS */: state.errors.value = message.payload; return; case 7 /* ACTION_TYPE.SET_FIELD_ERROR */: set(state.errors.value, message.payload.path, message.payload.error); return; case 8 /* ACTION_TYPE.SET_ISSUBMITTING */: state.isSubmitting.value = message.payload; return; case 9 /* ACTION_TYPE.SET_ISVALIDATING */: state.isValidating.value = message.payload; return; case 10 /* ACTION_TYPE.RESET_FORM */: reducer(state, { type: 3 /* ACTION_TYPE.SET_VALUES */, payload: message.payload.values, }); state.touched.value = message.payload.touched; state.errors.value = message.payload.errors; state.submitCount.value = message.payload.submitCount; } } const emptyErrors = {}; const emptyTouched = {}; /** * Custom composition API to mange the entire form. * * @param options - form configuration and validation parameters. {@link UseFormOptions} * * @returns methods and state of this form. {@link UseFormReturn} * * @example * ```vue * <script setup lang="ts"> * const { register, handleSubmit } = useForm({ * initialValues: { * name: 'Alex', * age: 18, * }, * onSubmit (values) { * console.log({ values }) * }, * }); * * const { value: name, attrs: nameAttrs } = register('name') * const { value: age, attrs: ageAttrs } = register('name') * </script> * * <template> * <form v-on:submit="handleSubmit"> * <input v-model="name" type="text" v-bind="nameAttrs" /> * <input v-model.number="age" type="text" v-bind="ageAttrs" /> * <input type="submit" /> * </form> * </template> * ``` */ function useForm(options) { const { validateOnMounted = false, validateMode = 'submit', reValidateMode = 'change', onSubmit, onInvalid, } = options; let initialValues = klona(options.initialValues); let initialErrors = klona(options.initialErrors || emptyErrors); let initialTouched = klona(options.initialTouched || emptyTouched); const [state, dispatch] = useFormStore(reducer, { values: reactive(klona(initialValues)), errors: ref(klona(initialErrors)), touched: ref(klona(initialTouched)), submitCount: ref(0), isSubmitting: ref(false), isValidating: ref(false), }); const fieldRegistry = {}; const fieldArrayRegistry = {}; const dirty = computed(() => !es6(state.values, initialValues)); const validateTiming = computed(() => state.submitCount.value === 0 ? validateMode : reValidateMode); const registerField = (name, { validate } = {}) => { fieldRegistry[unref(name)] = { validate, }; }; const registerFieldArray = (name, options) => { registerField(name, options); fieldArrayRegistry[unref(name)] = { reset: options.reset, }; }; const setFieldTouched = (name, touched = true) => { dispatch({ type: 5 /* ACTION_TYPE.SET_TOUCHED */, payload: { path: name, touched, }, }); return validateTiming.value === 'blur' ? runAllValidateHandler(state.values) : Promise.resolve(); }; const setValues = (values, shouldValidate) => { dispatch({ type: 3 /* ACTION_TYPE.SET_VALUES */, payload: values, }); const willValidate = shouldValidate == null ? validateTiming.value === 'change' : shouldValidate; return willValidate ? runAllValidateHandler(state.values) : Promise.resolve(); }; const setFieldValue = (name, value, shouldValidate) => { dispatch({ type: 4 /* ACTION_TYPE.SET_FIELD_VALUE */, payload: { path: name, value, }, }); return shouldValidate ? runAllValidateHandler(state.values) : Promise.resolve(); }; const setFieldArrayValue = (name, value, method, args, shouldSetValue = true) => { if (method && args) { if (keysOf(state.errors.value).length && Array.isArray(get(state.errors.value, name))) { const error = method(get(state.errors.value, name), args.argA, args.argB); if (shouldSetValue) { dispatch({ type: 7 /* ACTION_TYPE.SET_FIELD_ERROR */, payload: { path: name, error, }, }); } } if (keysOf(state.touched.value).length && Array.isArray(get(state.touched.value, name))) { const touched = method(get(state.touched.value, name), args.argA, args.argB); if (shouldSetValue) { dispatch({ type: 5 /* ACTION_TYPE.SET_TOUCHED */, payload: { path: name, touched, }, }); } } } return setFieldValue(name, value); }; const handleBlur = (eventOrName, path) => { if (isString(eventOrName)) { return () => setFieldTouched(eventOrName, true); } const { name, id } = eventOrName.target; const field = path !== null && path !== void 0 ? path : (name || id); if (field) { setFieldTouched(field, true); } }; const handleChange = () => { if (validateTiming.value === 'change') { runAllValidateHandler(state.values); } }; const handleInput = () => { if (validateTiming.value === 'input') { runAllValidateHandler(state.values); } }; const setSubmitting = (isSubmitting) => { dispatch({ type: 8 /* ACTION_TYPE.SET_ISSUBMITTING */, payload: isSubmitting }); }; const getFieldValue = (name) => { return computed({ get() { return get(state.values, unref(name)); }, set(value) { setFieldValue(unref(name), value); }, }); }; const getFieldMeta = (name) => { const error = computed(() => getFieldError(unref(name))); const touched = computed(() => getFieldTouched(unref(name))); const dirty = computed(() => getFieldDirty(unref(name))); return { dirty, error, touched, }; }; const getFieldAttrs = (name) => { return computed(() => ({ name: unref(name), onBlur: handleBlur, onChange: handleChange, onInput: handleInput, })); }; const getFieldError = (name) => { return get(state.errors.value, name); }; const getFieldTouched = (name) => { return get(state.touched.value, name, false); }; const getFieldDirty = (name) => { return !es6(get(initialValues, name), get(state.values, name)); }; const submitHelper = { setSubmitting, get initialValues() { return klona(initialValues); }, }; const runSingleFieldValidateHandler = (name, value) => { return new Promise((resolve) => resolve(fieldRegistry[name].validate(value))); }; const runFieldValidateHandler = (values) => { const fieldKeysWithValidation = keysOf(fieldRegistry).filter((field) => isFunction(fieldRegistry[field].validate)); const fieldValidatePromise = fieldKeysWithValidation.map((field) => runSingleFieldValidateHandler(field, get(values, field))); return Promise.all(fieldValidatePromise).then((errors) => errors.reduce((prev, curr, index) => { if (curr) { set(prev, fieldKeysWithValidation[index], curr); } return prev; }, {})); }; const runValidateHandler = (values) => { return new Promise((resolve) => { var _a; const maybePromise = (_a = options.validate) === null || _a === void 0 ? void 0 : _a.call(options, values); if (maybePromise == null) { resolve({}); } else if (isPromise(maybePromise)) { maybePromise.then((error) => { resolve(error || {}); }); } else { resolve(maybePromise); } }); }; const runAllValidateHandler = (values = state.values) => { dispatch({ type: 9 /* ACTION_TYPE.SET_ISVALIDATING */, payload: true }); return Promise.all([ runFieldValidateHandler(values), options.validate ? runValidateHandler(values) : {}, ]) .then(([fieldErrors, validateErrors]) => { const errors = cjs.all([fieldErrors, validateErrors], { arrayMerge, }); dispatch({ type: 6 /* ACTION_TYPE.SET_ERRORS */, payload: errors }); return errors; }) .finally(() => { dispatch({ type: 9 /* ACTION_TYPE.SET_ISVALIDATING */, payload: false }); }); }; const handleSubmit = (event) => { event === null || event === void 0 ? void 0 : event.preventDefault(); dispatch({ type: 0 /* ACTION_TYPE.SUBMIT_ATTEMPT */ }); runAllValidateHandler().then((errors) => { const isValid = keysOf(errors).length === 0; if (isValid) { const maybePromise = onSubmit(klona(state.values), submitHelper); if (maybePromise == null) { return; } maybePromise .then((result) => { dispatch({ type: 1 /* ACTION_TYPE.SUBMIT_SUCCESS */ }); return result; }) .catch(() => { dispatch({ type: 2 /* ACTION_TYPE.SUBMIT_FAILURE */ }); }); } else { dispatch({ type: 2 /* ACTION_TYPE.SUBMIT_FAILURE */ }); onInvalid === null || onInvalid === void 0 ? void 0 : onInvalid(errors); } }); }; const resetForm = (nextState) => { const values = klona((nextState === null || nextState === void 0 ? void 0 : nextState.values) || initialValues); const errors = klona((nextState === null || nextState === void 0 ? void 0 : nextState.errors) || initialErrors); const touched = klona((nextState === null || nextState === void 0 ? void 0 : nextState.touched) || initialTouched); initialValues = klona(values); initialErrors = klona(errors); initialTouched = klona(touched); dispatch({ type: 10 /* ACTION_TYPE.RESET_FORM */, payload: { values, touched, errors, submitCount: typeof (nextState === null || nextState === void 0 ? void 0 : nextState.submitCount) === 'number' ? nextState.submitCount : 0, }, }); // reset `fields` of `useFieldArray` Object.values(fieldArrayRegistry).forEach((field) => { field.reset(); }); }; const handleReset = (event) => { event === null || event === void 0 ? void 0 : event.preventDefault(); resetForm(); }; const register = (name, options) => { registerField(name, options); return { value: getFieldValue(name), attrs: getFieldAttrs(name), ...getFieldMeta(name), }; }; const validateField = (name) => { if (fieldRegistry[name] && isFunction(fieldRegistry[name].validate)) { dispatch({ type: 9 /* ACTION_TYPE.SET_ISVALIDATING */, payload: true }); return runSingleFieldValidateHandler(name, get(state.values, name)) .then((error) => { dispatch({ type: 7 /* ACTION_TYPE.SET_FIELD_ERROR */, payload: { path: name, error }, }); }) .finally(() => { dispatch({ type: 9 /* ACTION_TYPE.SET_ISVALIDATING */, payload: false }); }); } return Promise.resolve(); }; const context = { values: state.values, touched: computed(() => state.touched.value), errors: computed(() => state.errors.value), submitCount: computed(() => state.submitCount.value), isSubmitting: state.isSubmitting, isValidating: computed(() => state.isValidating.value), dirty, register, setValues, setFieldValue, handleSubmit, handleReset, resetForm, validateForm: runAllValidateHandler, validateField, }; provide(InternalContextKey, { getFieldMeta, getFieldValue, setFieldValue, getFieldError, getFieldTouched, getFieldDirty, getFieldAttrs, registerField, registerFieldArray, setFieldArrayValue, }); provide(FormContextKey, context); onMounted(() => { if (!validateOnMounted) return; runAllValidateHandler(initialValues); }); return context; } /** * deepmerge array merging algorithm * https://github.com/TehShrike/deepmerge#arraymerge-example-combine-arrays */ function arrayMerge(target, source, options) { const destination = [...target]; source.forEach((item, index) => { if (typeof destination[index] === 'undefined') { destination[index] = options.cloneUnlessOtherwiseSpecified(item, options); } else if (options.isMergeableObject(item)) { destination[index] = cjs(target[index], item, options); } else if (target.indexOf(item) === -1) { destination.push(item); } }); return destination; } /** * Custom composition API that will return specific field value, meta (state) and attributes. * * @param name - field name * * @param options - field configuration and validation parameters. {@link UseFieldOptions} * * @returns field value, meta (state) and attributes. {@link UseFormRegisterReturn} * * @example * ```vue * <script setup lang="ts"> * const { handleSubmit } = useForm({ * initialValues: { * name: '', * }, * onSubmit(values) { * console.log({ values }); * }, * }); * * const { value: name, attrs: nameAttrs } = useField('name') * </script> * * <template> * <form v-on:submit="handleSubmit"> * <input v-model="name" type="text" v-bind="nameAttrs" /> * <input type="submit" /> * </form> * </template> * ``` */ function useField(name, options = {}) { const { registerField, getFieldValue, getFieldAttrs, getFieldMeta } = useInternalContext(); registerField(name, options); return { value: getFieldValue(name), attrs: getFieldAttrs(name), ...getFieldMeta(name), }; } var appendAt = (data, value) => { return [...data, value]; }; var insertAt = (data, index, value) => { return [...data.slice(0, index), value, ...data.slice(index)]; }; var moveAt = (data, from, to) => { data.splice(to, 0, data.splice(from, 1)[0]); }; var omit = (source, key) => { const copy = { ...source }; delete copy[key]; return copy; }; var prependAt = (data, value) => { return [value, ...data]; }; var removeAt = (data, index) => { if (isUndefined(index)) return []; const clone = [...data]; clone.splice(index, 1); return clone; }; var swapAt = (data, indexA, indexB) => { data[indexA] = [data[indexB], (data[indexB] = data[indexA])][0]; }; var updateAt = (data, index, value) => { const clone = [...data]; clone[index] = value; return clone; }; /** * Custom composition API that exposes convenient to perform operations with a list of fields. * * @param name - field name * * @param options - field array configuration and validation parameters {@link UseFieldArrayOptions} * * @returns fields state and methods. {@link UseFieldArrayReturn} * * @example * ```vue * <script setup lang="ts"> * import { useForm, useFieldArray } from '@vorms/core'; * * const { values, handleSubmit } = useForm({ * initialValues: { * list: ['vue', 'vue-router'], * }, * onSubmit(values) { * console.log({ values }); * }, * }); * * const { fields, append } = useFieldArray('list'); * </script> * * <template> * <pre>{{ values }}</pre> * <form v-on:submit="handleSubmit"> * <template v-for="field in fields" :key="field.key"> * <input v-model="field.value" :name="field.name" v-bind="field.attrs" /> * </template> * <button type="button" v-on:click="append('pinia')">Append</button> * <input type="submit" /> * </form> * </template> * ``` */ function useFieldArray(name, options) { const { getFieldValue, setFieldValue, getFieldError, getFieldTouched, getFieldDirty, getFieldAttrs, registerFieldArray, setFieldArrayValue, } = useInternalContext(); const fields = ref([]); const values = computed(() => { return getFieldValue(name).value || []; }); let seed = 0; const reset = () => { fields.value = values.value.map(createEntry); }; const createEntry = (value) => { const key = seed++; const index = computed(() => fields.value.findIndex((field) => field.key === key)); return { key, value: computed({ get() { return index.value === -1 ? value : values.value[index.value]; }, set(value) { if (index.value === -1) return; setFieldValue(`${name}.${index.value}`, value); }, }), name: computed(() => { return `${unref(name)}.${index.value}`; }), error: computed(() => { return getFieldError(`${unref(name)}.${index.value}`); }), touched: computed(() => { return getFieldTouched(`${unref(name)}.${index.value}`); }), dirty: computed(() => { return getFieldDirty(`${unref(name)}.${index.value}`); }), attrs: computed(() => { return omit(unref(getFieldAttrs(`${unref(name)}.${index.value}`)), 'name'); }), }; // `computed` will be auto unwrapped }; const append = (value) => { setFieldArrayValue(unref(name), appendAt(values.value, value), appendAt, { argA: undefined, }); fields.value.push(createEntry(value)); }; const prepend = (value) => { setFieldArrayValue(unref(name), prependAt(values.value, value), prependAt, { argA: undefined, }); fields.value.unshift(createEntry(value)); }; const remove = (index) => { const cloneValues = removeAt(values.value, index); const cloneField = removeAt(fields.value, index); setFieldArrayValue(unref(name), cloneValues, removeAt, { argA: index, }); fields.value = cloneField; }; const swap = (indexA, indexB) => { if (!(indexA in values.value) || !(indexB in values.value)) return; const cloneValues = [...values.value]; const cloneField = [...fields.value]; swapAt(cloneValues, indexA, indexB); swapAt(cloneField, indexA, indexB); setFieldArrayValue(unref(name), cloneValues, swapAt, { argA: indexA, argB: indexB, }, false); fields.value = cloneField; }; const move = (from, to) => { if (!(from in values.value)) return; const cloneValues = [...values.value]; const cloneField = [...fields.value]; moveAt(cloneValues, from, to); moveAt(cloneField, from, to); setFieldArrayValue(unref(name), cloneValues, moveAt, { argA: from, argB: to, }, false); fields.value = cloneField; }; const insert = (index, value) => { const cloneValues = insertAt(values.value, index, value); const cloneField = insertAt(fields.value, index, createEntry(value)); setFieldArrayValue(unref(name), cloneValues, insertAt, { argA: index, argB: undefined, }); fields.value = cloneField; }; const update = (index, value) => { if (!(index in values.value)) return; const cloneValue = updateAt(values.value, index, value); setFieldArrayValue(unref(name), cloneValue, updateAt, { argA: index, argB: undefined, }); fields.value[index].value = value; }; const replace = (values) => { const cloneValues = [...values]; setFieldArrayValue(unref(name), cloneValues, (data) => data, {}); fields.value = cloneValues.map(createEntry); }; registerFieldArray(name, { ...options, reset, }); reset(); return { fields, append, prepend, swap, remove, move, insert, update, replace, }; } export { get, set, useField, useFieldArray, useForm, useFormContext };