147 lines
5.1 KiB
TypeScript
147 lines
5.1 KiB
TypeScript
import { computed, ref, toValue, watch } from 'vue';
|
|
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
|
|
import { insert as insertAt, move as moveItem, remove as removeAt, swap as swapItems } from '@robonen/stdlib';
|
|
import { injectFormContext } from '../useForm/context';
|
|
import type {
|
|
FieldArrayEntry,
|
|
UseFieldArrayOptions,
|
|
UseFieldArrayReturn,
|
|
} from '../useForm';
|
|
|
|
/**
|
|
* @name useFieldArray
|
|
* @category Forms
|
|
* @description Manage a dynamic array field within a {@link useForm}. Exposes a
|
|
* reactive `fields` list with **stable keys** (preserved across reorders, so
|
|
* `v-for :key` keeps DOM/state intact) plus immutable `push`/`prepend`/`insert`/
|
|
* `remove`/`move`/`swap`/`replace`/`update` operations that also re-key the
|
|
* matching errors and touched state.
|
|
*
|
|
* @param {MaybeRefOrGetter<string>} path The dotted path of the array field
|
|
* @param {UseFieldArrayOptions} [options={}] Optionally an explicit `form`
|
|
* @returns {UseFieldArrayReturn} The reactive entries and mutation helpers
|
|
*
|
|
* @example
|
|
* const form = useForm({ initialValues: { users: [{ name: '' }] } });
|
|
* const { fields, push, remove } = useFieldArray('users');
|
|
* // <div v-for="(field, i) in fields" :key="field.key">
|
|
* // <input v-model="field.value.name"> <button @click="remove(i)">x</button>
|
|
* // </div>
|
|
* // <button @click="push({ name: '' })">add</button>
|
|
*
|
|
* @since 0.0.14
|
|
*/
|
|
export function useFieldArray<T = unknown>(
|
|
path: MaybeRefOrGetter<string>,
|
|
options: UseFieldArrayOptions = {},
|
|
): UseFieldArrayReturn<T> {
|
|
const form = options.form ?? injectFormContext();
|
|
|
|
if (!form)
|
|
throw new Error('[useFieldArray] must be used within a useForm() provider or given an explicit `form`.');
|
|
|
|
const resolvePath = (): string => toValue(path);
|
|
|
|
function currentArray(): T[] {
|
|
return (form!.getFieldValue(resolvePath() as never) as T[] | undefined) ?? [];
|
|
}
|
|
|
|
// Stable keys parallel to the values array — one per item, preserved on reorder.
|
|
let keyCounter = 0;
|
|
const keys = ref<number[]>(currentArray().map(() => keyCounter++));
|
|
|
|
// Reconcile keys if the array length changes from outside our own ops.
|
|
watch(() => currentArray().length, (length) => {
|
|
const current = keys.value;
|
|
if (length > current.length) {
|
|
const next = current.slice();
|
|
while (next.length < length)
|
|
next.push(keyCounter++);
|
|
keys.value = next;
|
|
}
|
|
else if (length < current.length) {
|
|
keys.value = current.slice(0, length);
|
|
}
|
|
});
|
|
|
|
function indexRef(index: number): Ref<T> {
|
|
return computed<T>({
|
|
get: () => (form!.getFieldValue(`${resolvePath()}.${index}` as never) as T),
|
|
set: value => form!.setFieldValue(`${resolvePath()}.${index}` as never, value as never, { shouldValidate: false }),
|
|
}) as unknown as Ref<T>;
|
|
}
|
|
|
|
const fields = computed<Array<FieldArrayEntry<T>>>(() => {
|
|
const length = currentArray().length;
|
|
const keyList = keys.value;
|
|
|
|
return Array.from({ length }, (_unused, index) => ({
|
|
key: keyList[index] ?? index,
|
|
value: indexRef(index),
|
|
isFirst: index === 0,
|
|
isLast: index === length - 1,
|
|
}));
|
|
});
|
|
|
|
function writeArray(nextValues: T[], nextKeys: number[]): void {
|
|
form!.setFieldValue(resolvePath() as never, nextValues as never, { shouldValidate: false });
|
|
keys.value = nextKeys;
|
|
}
|
|
|
|
function push(value: T): void {
|
|
writeArray([...currentArray(), value], [...keys.value, keyCounter++]);
|
|
}
|
|
|
|
function prepend(value: T): void {
|
|
writeArray([value, ...currentArray()], [keyCounter++, ...keys.value]);
|
|
form!._remapFieldPaths(resolvePath(), index => index + 1);
|
|
}
|
|
|
|
function insert(index: number, value: T): void {
|
|
writeArray(insertAt(currentArray(), index, value), insertAt(keys.value, index, keyCounter++));
|
|
form!._remapFieldPaths(resolvePath(), current => (current >= index ? current + 1 : current));
|
|
}
|
|
|
|
function remove(index: number): void {
|
|
writeArray(removeAt(currentArray(), index), removeAt(keys.value, index));
|
|
form!._remapFieldPaths(resolvePath(), current =>
|
|
current === index ? null : current > index ? current - 1 : current);
|
|
}
|
|
|
|
function move(from: number, to: number): void {
|
|
writeArray(moveItem(currentArray(), from, to), moveItem(keys.value, from, to));
|
|
const order = moveItem(Array.from({ length: currentArray().length }, (_unused, index) => index), from, to);
|
|
form!._remapFieldPaths(resolvePath(), current => order.indexOf(current));
|
|
}
|
|
|
|
function swap(indexA: number, indexB: number): void {
|
|
writeArray(swapItems(currentArray(), indexA, indexB), swapItems(keys.value, indexA, indexB));
|
|
form!._remapFieldPaths(resolvePath(), current =>
|
|
current === indexA ? indexB : current === indexB ? indexA : current);
|
|
}
|
|
|
|
function replace(values: T[]): void {
|
|
writeArray([...values], values.map(() => keyCounter++));
|
|
}
|
|
|
|
function update(index: number, value: T): void {
|
|
const next = currentArray().slice();
|
|
if (index < 0 || index >= next.length)
|
|
return;
|
|
next[index] = value;
|
|
writeArray(next, keys.value.slice());
|
|
}
|
|
|
|
return {
|
|
fields: fields as ComputedRef<Array<FieldArrayEntry<T>>>,
|
|
push,
|
|
prepend,
|
|
insert,
|
|
remove,
|
|
move,
|
|
swap,
|
|
replace,
|
|
update,
|
|
};
|
|
}
|