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} 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'); * //
* // * //
* // * * @since 0.0.14 */ export function useFieldArray( path: MaybeRefOrGetter, options: UseFieldArrayOptions = {}, ): UseFieldArrayReturn { 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(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 { return computed({ 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; } const fields = computed>>(() => { 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>>, push, prepend, insert, remove, move, swap, replace, update, }; }