import type { FormApi, FormAsyncValidateOrFn, FormValidateOrFn, } from './FormApi' import type { AnyFieldMeta } from './FieldApi' import type { DeepKeys } from './util-types' type ValueFieldMode = 'insert' | 'remove' | 'swap' | 'move' export const defaultFieldMeta: AnyFieldMeta = { isValidating: false, isTouched: false, isBlurred: false, isDirty: false, isPristine: true, isValid: true, isDefaultValue: true, errors: [], errorMap: {}, errorSourceMap: {}, _arrayVersion: 0, } export function metaHelper< TFormData, TOnMount extends undefined | FormValidateOrFn, TOnChange extends undefined | FormValidateOrFn, TOnChangeAsync extends undefined | FormAsyncValidateOrFn, TOnBlur extends undefined | FormValidateOrFn, TOnBlurAsync extends undefined | FormAsyncValidateOrFn, TOnSubmit extends undefined | FormValidateOrFn, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, TOnDynamic extends undefined | FormValidateOrFn, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, TOnServer extends undefined | FormAsyncValidateOrFn, TSubmitMeta = never, >( formApi: FormApi< TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta >, ) { /** * Bump the `_arrayVersion` counter on the array field's meta. This * provides a cheap, structural signal that adapters can subscribe to in * order to trigger re-renders when the array is mutated in ways that * `length` alone cannot detect (e.g. swaps and moves). */ function bumpArrayVersion(field: DeepKeys) { const currentMeta = formApi.getFieldMeta(field) ?? defaultFieldMeta formApi.setFieldMeta(field, { ...currentMeta, _arrayVersion: (currentMeta._arrayVersion || 0) + 1, }) } /** * Handle the meta shift caused from moving a field from one index to another. */ function handleArrayMove( field: DeepKeys, fromIndex: number, toIndex: number, ) { bumpArrayVersion(field) const affectedFields = getAffectedFields(field, fromIndex, 'move', toIndex) const startIndex = Math.min(fromIndex, toIndex) const endIndex = Math.max(fromIndex, toIndex) for (let i = startIndex; i <= endIndex; i++) { affectedFields.push(getFieldPath(field, i)) } // Store the original field meta that will be reapplied at the destination index const fromFields = Object.keys(formApi.fieldInfo).reduce( (fieldMap, fieldKey) => { if (fieldKey.startsWith(getFieldPath(field, fromIndex))) { fieldMap.set( fieldKey as DeepKeys, formApi.getFieldMeta(fieldKey as DeepKeys), ) } return fieldMap }, new Map, AnyFieldMeta | undefined>(), ) shiftMeta(affectedFields, fromIndex < toIndex ? 'up' : 'down') // Reapply the stored field meta at the destination index Object.keys(formApi.fieldInfo) .filter((fieldKey) => fieldKey.startsWith(getFieldPath(field, toIndex))) .forEach((fieldKey) => { const fromKey = fieldKey.replace( getFieldPath(field, toIndex), getFieldPath(field, fromIndex), ) as DeepKeys const fromMeta = fromFields.get(fromKey) if (fromMeta) { formApi.setFieldMeta(fieldKey as DeepKeys, fromMeta) } }) } /** * Handle the meta shift from removing a field at the specified index. */ function handleArrayRemove(field: DeepKeys, index: number) { bumpArrayVersion(field) const affectedFields = getAffectedFields(field, index, 'remove') shiftMeta(affectedFields, 'up') } /** * Handle the meta shift from swapping two fields at the specified indeces. */ function handleArraySwap( field: DeepKeys, index: number, secondIndex: number, ) { bumpArrayVersion(field) const affectedFields = getAffectedFields(field, index, 'swap', secondIndex) affectedFields.forEach((fieldKey) => { if (!fieldKey.toString().startsWith(getFieldPath(field, index))) { return } const swappedKey = fieldKey .toString() .replace( getFieldPath(field, index), getFieldPath(field, secondIndex), ) as DeepKeys const [meta1, meta2] = [ formApi.getFieldMeta(fieldKey), formApi.getFieldMeta(swappedKey), ] if (meta1) formApi.setFieldMeta(swappedKey, meta1) if (meta2) formApi.setFieldMeta(fieldKey, meta2) }) } /** * Handle the meta shift from inserting a field at the specified index. */ function handleArrayInsert(field: DeepKeys, insertIndex: number) { bumpArrayVersion(field) const affectedFields = getAffectedFields(field, insertIndex, 'insert') shiftMeta(affectedFields, 'down') affectedFields.forEach((fieldKey) => { if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) { formApi.setFieldMeta(fieldKey, getEmptyFieldMeta()) } }) } function getFieldPath( field: DeepKeys, index: number, ): DeepKeys { return `${field}[${index}]` as DeepKeys } function getAffectedFields( field: DeepKeys, index: number, mode: ValueFieldMode, secondIndex?: number, ): DeepKeys[] { const affectedFieldKeys = [getFieldPath(field, index)] switch (mode) { case 'swap': affectedFieldKeys.push(getFieldPath(field, secondIndex!)) break case 'move': { const [startIndex, endIndex] = [ Math.min(index, secondIndex!), Math.max(index, secondIndex!), ] for (let i = startIndex; i <= endIndex; i++) { affectedFieldKeys.push(getFieldPath(field, i)) } break } default: { const currentValue = formApi.getFieldValue(field) const fieldItems = Array.isArray(currentValue) ? (currentValue as Array).length : 0 for (let i = index + 1; i < fieldItems; i++) { affectedFieldKeys.push(getFieldPath(field, i)) } break } } return Object.keys(formApi.fieldInfo).filter((fieldKey) => affectedFieldKeys.some((key) => fieldKey.startsWith(key)), ) as DeepKeys[] } function updateIndex( fieldKey: string, direction: 'up' | 'down', ): DeepKeys { return fieldKey.replace(/\[(\d+)\]/, (_, num) => { const currIndex = parseInt(num, 10) const newIndex = direction === 'up' ? currIndex + 1 : Math.max(0, currIndex - 1) return `[${newIndex}]` }) as DeepKeys } function shiftMeta(fields: DeepKeys[], direction: 'up' | 'down') { const sortedFields = direction === 'up' ? fields : [...fields].reverse() sortedFields.forEach((fieldKey) => { const nextFieldKey = updateIndex(fieldKey.toString(), direction) const nextFieldMeta = formApi.getFieldMeta(nextFieldKey) if (nextFieldMeta) { formApi.setFieldMeta(fieldKey, nextFieldMeta) } else { formApi.setFieldMeta(fieldKey, getEmptyFieldMeta()) } }) } const getEmptyFieldMeta = (): AnyFieldMeta => defaultFieldMeta return { bumpArrayVersion, handleArrayMove, handleArrayRemove, handleArraySwap, handleArrayInsert, } }