@vorms/core
Version:
Vue Form Validation with Composition API
1,037 lines (963 loc) • 34.4 kB
JavaScript
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 };