From 506523e76e4146929b97e50b33874f3280cd4ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20K=C3=BCsgen?= Date: Thu, 17 Apr 2025 08:29:02 +0200 Subject: [PATCH 1/2] feat(form-core): add array method `field.filterValues` and `form.filterFieldValues` Co-authored-by: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> --- packages/form-core/src/FieldApi.ts | 21 ++++ packages/form-core/src/FormApi.ts | 81 +++++++++++++++ packages/form-core/src/metaHelper.ts | 90 ++++++++++++---- packages/form-core/tests/FieldApi.spec.ts | 119 ++++++++++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 102 +++++++++++++++++++ 5 files changed, 395 insertions(+), 18 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index a95fd5515..88f12b63f 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1288,6 +1288,27 @@ export class FieldApi< this.triggerOnChangeListener() } + /** + * Filter values in the array using the provided predicate callback. + * @param predicate — The predicate callback to pass to the array's filter function. + * @param opts + */ + filterValues = ( + predicate: ( + value: TData extends Array ? TData[number] : never, + index: number, + array: TData, + ) => boolean, + opts?: UpdateMetaOptions & { + /** `thisArg` — An object to which the `this` keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the `this` value. */ + thisArg?: any + }, + ) => { + this.form.filterFieldValues(this.name, predicate, opts) + + this.triggerOnChangeListener() + } + /** * @private */ diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index e95b295f2..901dbc223 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1962,6 +1962,87 @@ export class FormApi< this.validateField(`${field}[${index2}]` as DeepKeys, 'change') } + /** + * Filter the array values by the provided predicate. + * @param field + * @param predicate — The predicate callback to pass to the array's filter function. + * @param opts + */ + filterFieldValues = < + TField extends DeepKeys, + TData extends DeepValue, + >( + field: TField, + predicate: ( + value: TData extends Array ? TData[number] : never, + index: number, + array: TData, + ) => boolean, + opts?: UpdateMetaOptions & { + /** `thisArg` — An object to which the `this` keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the `this` value. */ + thisArg?: any + }, + ) => { + const { thisArg, ...metaOpts } = opts ?? {} + const fieldValue = this.getFieldValue(field) + + const arrayData = { + previousLength: Array.isArray(fieldValue) + ? (fieldValue as unknown[]).length + : null, + validateFromIndex: null as number | null, + } + + const remainingIndeces: number[] = [] + + const filterFunction = + opts?.thisArg === undefined ? predicate : predicate.bind(opts.thisArg) + + this.setFieldValue( + field, + (prev: any) => + prev.filter((value: any, index: number, array: TData) => { + const keepElement = filterFunction(value, index, array) + if (!keepElement) { + // remember the first index that got filtered + arrayData.validateFromIndex ??= index + return false + } + remainingIndeces.push(index) + return true + }), + metaOpts, + ) + + // Shift meta accounting for filtered values + metaHelper(this).handleArrayFieldMetaShift( + field, + remainingIndeces, + 'filter', + ) + + // remove dangling fields if the filter call reduced the length of the array + if ( + arrayData.previousLength !== null && + remainingIndeces.length !== arrayData.previousLength + ) { + for (let i = remainingIndeces.length; i < arrayData.previousLength; i++) { + const fieldKey = `${field}[${i}]` + this.deleteField(fieldKey as never) + } + } + + // validate the array and the fields starting from the shifted elements + this.validateField(field, 'change') + if (arrayData.validateFromIndex !== null) { + this.validateArrayFieldsStartingFrom( + field, + arrayData.validateFromIndex, + 'change', + ) + } + } + /** * Resets the field value and meta to default state */ diff --git a/packages/form-core/src/metaHelper.ts b/packages/form-core/src/metaHelper.ts index eaa89af09..e1ffc5e12 100644 --- a/packages/form-core/src/metaHelper.ts +++ b/packages/form-core/src/metaHelper.ts @@ -6,7 +6,8 @@ import type { import type { AnyFieldMeta } from './FieldApi' import type { DeepKeys } from './util-types' -type ArrayFieldMode = 'insert' | 'remove' | 'swap' | 'move' +type ValueFieldMode = 'insert' | 'remove' | 'swap' | 'move' +type ArrayFieldMode = 'filter' export const defaultFieldMeta: AnyFieldMeta = { isValidating: false, @@ -45,34 +46,63 @@ export function metaHelper< ) { function handleArrayFieldMetaShift( field: DeepKeys, - index: number, + remainingIndeces: number[], mode: ArrayFieldMode, + ): void + function handleArrayFieldMetaShift( + field: DeepKeys, + index: number, + mode: ValueFieldMode, + secondIndex?: number, + ): void + function handleArrayFieldMetaShift( + field: DeepKeys, + index: number | number[], + mode: ArrayFieldMode | ValueFieldMode, secondIndex?: number, ) { - const affectedFields = getAffectedFields(field, index, mode, secondIndex) - - const handlers = { - insert: () => handleInsertMode(affectedFields, field, index), - remove: () => handleRemoveMode(affectedFields), - swap: () => - secondIndex !== undefined && - handleSwapMode(affectedFields, field, index, secondIndex), - move: () => - secondIndex !== undefined && - handleMoveMode(affectedFields, field, index, secondIndex), - } + if (Array.isArray(index)) { + if (mode === 'filter') { + return handleFilterMode(field, index) + } + } else { + const affectedFields = getAffectedFields( + field, + index, + mode as ValueFieldMode, + secondIndex, + ) - handlers[mode]() + switch (mode as ValueFieldMode) { + case 'insert': + return handleInsertMode(affectedFields, field, index) + case 'remove': + return handleRemoveMode(affectedFields) + case 'swap': + return ( + secondIndex !== undefined && + handleSwapMode(affectedFields, field, index, secondIndex) + ) + case 'move': + return ( + secondIndex !== undefined && + handleMoveMode(affectedFields, field, index, secondIndex) + ) + } + } } - function getFieldPath(field: DeepKeys, index: number): string { - return `${field}[${index}]` + function getFieldPath( + field: DeepKeys, + index: number, + ): DeepKeys { + return `${field}[${index}]` as DeepKeys } function getAffectedFields( field: DeepKeys, index: number, - mode: ArrayFieldMode, + mode: ValueFieldMode, secondIndex?: number, ): DeepKeys[] { const affectedFieldKeys = [getFieldPath(field, index)] @@ -148,6 +178,30 @@ export function metaHelper< shiftMeta(fields, 'up') } + const handleFilterMode = ( + field: DeepKeys, + remainingIndices: number[], + ) => { + if (remainingIndices.length === 0) return + + // create a map between the index and its new location + remainingIndices.forEach((fromIndex, toIndex) => { + if (fromIndex === toIndex) return + // assign it the original meta + const fieldKey = getFieldPath(field, toIndex) + const originalFieldKey = getFieldPath(field, fromIndex) + const originalFieldMeta = formApi.getFieldMeta(originalFieldKey) + if (originalFieldMeta) { + formApi.setFieldMeta(fieldKey, originalFieldMeta) + } else { + formApi.setFieldMeta(fieldKey, { + ...getEmptyFieldMeta(), + isTouched: originalFieldKey as unknown as boolean, + }) + } + }) + } + const handleMoveMode = ( fields: DeepKeys[], field: DeepKeys, diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 4ed38a623..c7703bbe5 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -1195,6 +1195,9 @@ describe('field api', () => { field.moveValue(0, 1) expect(arr).toStrictEqual(['middle', 'end', 'start']) + + field.filterValues((value) => value !== 'start') + expect(arr).toStrictEqual(['middle', 'end']) }) it('should reset the form on a listener', () => { @@ -2051,4 +2054,120 @@ describe('field api', () => { field.parseValueWithSchemaAsync(z.any()) }).not.toThrowError() }) + + it('should filter array values using the predicate when calling filterValues', async () => { + const form = new FormApi({ + defaultValues: { + names: ['one', 'two', 'three'], + }, + }) + + form.mount() + + const field = new FieldApi({ + form, + name: 'names', + }) + + field.mount() + + field.filterValues((value) => value !== 'two') + expect(field.state.value).toStrictEqual(['one', 'three']) + + field.filterValues((value) => value !== 'never') + expect(field.state.value).toStrictEqual(['one', 'three']) + }) + + it('should bind the predicate to the provided thisArg when calling filterValues', async () => { + // Very dirty way, but quick way to enforce lost `this` context + function SomeClass(this: any) { + this.check = 'correct this' + } + SomeClass.prototype.filterFunc = function () { + return this?.check === 'correct this' + } + // @ts-expect-error The 'new' expression expects class stuff, but + // we're trying to force ugly code in this unit test. + const instance = new SomeClass() + + const predicate = instance.filterFunc + + const form = new FormApi({ + defaultValues: { + names: ['one', 'two', 'three'], + }, + }) + + const field = new FieldApi({ + form, + name: 'names', + }) + + form.mount() + field.mount() + + field.filterValues(predicate, { thisArg: instance }) + // thisArg was bound, expect it to have returned true + expect(field.state.value).toStrictEqual(['one', 'two', 'three']) + field.filterValues(predicate) + // thisArg wasn't bound, expect it to have returned false + expect(field.state.value).toStrictEqual([]) + }) + + it('should run onChange validation on the array when calling filterValues', async () => { + vi.useFakeTimers() + const form = new FormApi({ + defaultValues: { + names: ['one', 'two', 'three', 'four', 'five'], + }, + }) + form.mount() + function getField(i: number) { + return new FieldApi({ + name: `names[${i}]`, + form, + validators: { + onChange: () => 'error', + }, + }) + } + + const arrayField = new FieldApi({ + form, + name: 'names', + validators: { + onChange: () => 'error', + }, + }) + arrayField.mount() + + const field0 = getField(0) + const field1 = getField(1) + const field2 = getField(2) + const field3 = getField(3) + const field4 = getField(4) + field0.mount() + field1.mount() + field2.mount() + field3.mount() + field4.mount() + + arrayField.filterValues((value) => value !== 'three') + // validating fields is separate from filterValues and done with a promise, + // so make sure they resolve first + await vi.runAllTimersAsync() + + expect(arrayField.getMeta().errors).toStrictEqual(['error']) + + // field 0 and 1 weren't shifted, so they shouldn't trigger validation + expect(field0.getMeta().errors).toStrictEqual([]) + expect(field1.getMeta().errors).toStrictEqual([]) + + // but the following fields were shifted + expect(field2.getMeta().errors).toStrictEqual(['error']) + expect(field3.getMeta().errors).toStrictEqual(['error']) + + // field4 no longer exists, so it shouldn't have errors + expect(field4.getMeta().errors).toStrictEqual([]) + }) }) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 79ca9f797..d4b1a9c9f 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -3015,4 +3015,106 @@ describe('form api', () => { form.parseValuesWithSchemaAsync(z.any()) }).not.toThrowError() }) + + it('should filter fields when calling filterFieldValues', () => { + const form = new FormApi({ + defaultValues: { + items: ['a', 'b', 'c', 'd', 'e', 'f'], + }, + }) + + const fields = new Array(5) + .fill(0) + .map((_, i) => new FieldApi({ form, name: `items[${i}]` })) + + form.mount() + fields.forEach((field) => field.mount()) + + // Ensure the initial state has the fields + expect(form.getFieldValue('items')).toEqual(['a', 'b', 'c', 'd', 'e', 'f']) + + // Reset the field to an empty array + form.filterFieldValues('items', (v) => + [ + 'a', + 'b', + // 'c', + 'd', + // 'e', + 'f', + ].includes(v), + ) + + // Verify that the field value is now an empty array + expect(form.getFieldValue('items')).toEqual(['a', 'b', 'd', 'f']) + }) + + it('should shift meta when calling filterFieldValues', () => { + const form = new FormApi({ + defaultValues: { + items: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], + }, + }) + + const fields = new Array(10) + .fill(null) + .map((_, i) => new FieldApi({ form, name: `items[${i}]` })) + + form.mount() + fields.forEach((field) => field.mount()) + + /* + Before the filter, achieve the following touched state: + [ 'items[0]', 'a', false ], + [ 'items[1]', 'b', true ], + [ 'items[2]', 'c', false ], + [ 'items[3]', 'd', true ], + [ 'items[4]', 'e', false ], + [ 'items[5]', 'f', false ], + [ 'items[6]', 'g', true ], + [ 'items[7]', 'h', false ], + [ 'items[8]', 'i', true ], + [ 'items[9]', 'j', false ] + */ + function blurField(index: number) { + form.setFieldMeta(`items[${index}]`, (prev) => ({ + ...prev, + isBlurred: true, + })) + } + blurField(1) + blurField(3) + blurField(6) + blurField(8) + + form.filterFieldValues( + 'items', + (letter) => !['c', 'f', 'g'].includes(letter), + ) + + /** + The resulting array should now be + [ 'items[0]', 'a', false ], + [ 'items[1]', 'b', true ], + [ 'items[2]', 'd', true ], + [ 'items[3]', 'e', false ], + [ 'items[4]', 'h', false ], + [ 'items[5]', 'i', true ], + [ 'items[6]', 'j', false ], + */ + expect(form.getFieldValue(`items[${0}]`)).toBe('a') + expect(form.getFieldValue(`items[${1}]`)).toBe('b') + expect(form.getFieldValue(`items[${2}]`)).toBe('d') + expect(form.getFieldValue(`items[${3}]`)).toBe('e') + expect(form.getFieldValue(`items[${4}]`)).toBe('h') + expect(form.getFieldValue(`items[${5}]`)).toBe('i') + expect(form.getFieldValue(`items[${6}]`)).toBe('j') + expect(form.getFieldMeta(`items[${0}]`)?.isBlurred).toBe(false) + expect(form.getFieldMeta(`items[${1}]`)?.isBlurred).toBe(true) + expect(form.getFieldMeta(`items[${2}]`)?.isBlurred).toBe(true) + expect(form.getFieldMeta(`items[${3}]`)?.isBlurred).toBe(false) + expect(form.getFieldMeta(`items[${4}]`)?.isBlurred).toBe(false) + expect(form.getFieldMeta(`items[${5}]`)?.isBlurred).toBe(true) + expect(form.getFieldMeta(`items[${6}]`)?.isBlurred).toBe(false) + }) }) From a8b43d61720ebbd345b4cfe6f05b61e2d1f0b1c4 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:11:10 +0200 Subject: [PATCH 2/2] refactor(form-core): separate metaHelper function into multiple The previous implementation of metaHelper could lead to passing wrong index combinations with modes. Splitting them into separate functions allows for explicit requirements depending on mode. The previous implementation is preserved for compatability, but is marked as deprecated. --- packages/form-core/src/FormApi.ts | 14 +- packages/form-core/src/metaHelper.ts | 333 ++++++++++++++------------- 2 files changed, 181 insertions(+), 166 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 901dbc223..e58c44b52 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1837,7 +1837,7 @@ export class FormApi< await this.validateField(field, 'change') // Shift down all meta after validating to make sure the new field has been mounted - metaHelper(this).handleArrayFieldMetaShift(field, index, 'insert') + metaHelper(this).handleArrayInsert(field, index) await this.validateArrayFieldsStartingFrom(field, index, 'change') } @@ -1893,7 +1893,7 @@ export class FormApi< ) // Shift up all meta - metaHelper(this).handleArrayFieldMetaShift(field, index, 'remove') + metaHelper(this).handleArrayRemove(field, index) if (lastIndex !== null) { const start = `${field}[${lastIndex}]` @@ -1925,7 +1925,7 @@ export class FormApi< ) // Swap meta - metaHelper(this).handleArrayFieldMetaShift(field, index1, 'swap', index2) + metaHelper(this).handleArraySwap(field, index1, index2) // Validate the whole array this.validateField(field, 'change') @@ -1953,7 +1953,7 @@ export class FormApi< ) // Move meta between index1 and index2 - metaHelper(this).handleArrayFieldMetaShift(field, index1, 'move', index2) + metaHelper(this).handleArrayMove(field, index1, index2) // Validate the whole array this.validateField(field, 'change') @@ -2015,11 +2015,7 @@ export class FormApi< ) // Shift meta accounting for filtered values - metaHelper(this).handleArrayFieldMetaShift( - field, - remainingIndeces, - 'filter', - ) + metaHelper(this).handleArrayFilter(field, remainingIndeces) // remove dangling fields if the filter call reduced the length of the array if ( diff --git a/packages/form-core/src/metaHelper.ts b/packages/form-core/src/metaHelper.ts index e1ffc5e12..ecdd751c4 100644 --- a/packages/form-core/src/metaHelper.ts +++ b/packages/form-core/src/metaHelper.ts @@ -7,7 +7,6 @@ import type { AnyFieldMeta } from './FieldApi' import type { DeepKeys } from './util-types' type ValueFieldMode = 'insert' | 'remove' | 'swap' | 'move' -type ArrayFieldMode = 'filter' export const defaultFieldMeta: AnyFieldMeta = { isValidating: false, @@ -44,52 +43,163 @@ export function metaHelper< TSubmitMeta >, ) { - function handleArrayFieldMetaShift( + /** + * Handle the meta shift caused from moving a field from one index to another. + */ + function handleArrayMove( field: DeepKeys, - remainingIndeces: number[], - mode: ArrayFieldMode, - ): void + fromIndex: number, + toIndex: number, + ) { + 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) { + 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, + ) { + 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) { + const affectedFields = getAffectedFields(field, insertIndex, 'insert') + + shiftMeta(affectedFields, 'down') + + affectedFields.forEach((fieldKey) => { + if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) { + formApi.setFieldMeta(fieldKey, getEmptyFieldMeta()) + } + }) + } + + /** + * @deprecated Use the explicit functions of the metaHelper instead. + */ function handleArrayFieldMetaShift( field: DeepKeys, index: number, mode: ValueFieldMode, secondIndex?: number, - ): void - function handleArrayFieldMetaShift( + ) { + switch (mode) { + case 'insert': + return handleArrayInsert(field, index) + case 'remove': + return handleArrayRemove(field, index) + case 'swap': + return ( + secondIndex !== undefined && + handleArraySwap(field, index, secondIndex) + ) + case 'move': + return ( + secondIndex !== undefined && + handleArrayMove(field, index, secondIndex) + ) + } + } + + /** + * Handle the meta shift from filtering out indeces. + * @param remainingIndices An array of indeces that were NOT filtered out of the original array. + */ + function handleArrayFilter( field: DeepKeys, - index: number | number[], - mode: ArrayFieldMode | ValueFieldMode, - secondIndex?: number, + remainingIndices: number[], ) { - if (Array.isArray(index)) { - if (mode === 'filter') { - return handleFilterMode(field, index) - } - } else { - const affectedFields = getAffectedFields( - field, - index, - mode as ValueFieldMode, - secondIndex, - ) + if (remainingIndices.length === 0) return - switch (mode as ValueFieldMode) { - case 'insert': - return handleInsertMode(affectedFields, field, index) - case 'remove': - return handleRemoveMode(affectedFields) - case 'swap': - return ( - secondIndex !== undefined && - handleSwapMode(affectedFields, field, index, secondIndex) - ) - case 'move': - return ( - secondIndex !== undefined && - handleMoveMode(affectedFields, field, index, secondIndex) - ) + // create a map between the index and its new location + remainingIndices.forEach((fromIndex, toIndex) => { + if (fromIndex === toIndex) return + // assign it the original meta + const fieldKey = getFieldPath(field, toIndex) + const originalFieldKey = getFieldPath(field, fromIndex) + const originalFieldMeta = formApi.getFieldMeta(originalFieldKey) + if (originalFieldMeta) { + formApi.setFieldMeta(fieldKey, originalFieldMeta) + } else { + formApi.setFieldMeta(fieldKey, { + ...getEmptyFieldMeta(), + isTouched: originalFieldKey as unknown as boolean, + }) } - } + }) } function getFieldPath( @@ -107,23 +217,29 @@ export function metaHelper< ): DeepKeys[] { const affectedFieldKeys = [getFieldPath(field, index)] - if (mode === 'swap') { - affectedFieldKeys.push(getFieldPath(field, secondIndex!)) - } else if (mode === 'move') { - const [startIndex, endIndex] = [ - Math.min(index, secondIndex!), - Math.max(index, secondIndex!), - ] - for (let i = startIndex; i <= endIndex; i++) { - affectedFieldKeys.push(getFieldPath(field, i)) + 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 } - } else { - 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)) + 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 } } @@ -160,109 +276,12 @@ export function metaHelper< const getEmptyFieldMeta = (): AnyFieldMeta => defaultFieldMeta - const handleInsertMode = ( - fields: DeepKeys[], - field: DeepKeys, - insertIndex: number, - ) => { - shiftMeta(fields, 'down') - - fields.forEach((fieldKey) => { - if (fieldKey.toString().startsWith(getFieldPath(field, insertIndex))) { - formApi.setFieldMeta(fieldKey, getEmptyFieldMeta()) - } - }) - } - - const handleRemoveMode = (fields: DeepKeys[]) => { - shiftMeta(fields, 'up') - } - - const handleFilterMode = ( - field: DeepKeys, - remainingIndices: number[], - ) => { - if (remainingIndices.length === 0) return - - // create a map between the index and its new location - remainingIndices.forEach((fromIndex, toIndex) => { - if (fromIndex === toIndex) return - // assign it the original meta - const fieldKey = getFieldPath(field, toIndex) - const originalFieldKey = getFieldPath(field, fromIndex) - const originalFieldMeta = formApi.getFieldMeta(originalFieldKey) - if (originalFieldMeta) { - formApi.setFieldMeta(fieldKey, originalFieldMeta) - } else { - formApi.setFieldMeta(fieldKey, { - ...getEmptyFieldMeta(), - isTouched: originalFieldKey as unknown as boolean, - }) - } - }) - } - - const handleMoveMode = ( - fields: DeepKeys[], - field: DeepKeys, - fromIndex: number, - toIndex: number, - ) => { - // Store the original field meta that will be reapplied at the destination index - const fromFields = new Map( - Object.keys(formApi.fieldInfo) - .filter((fieldKey) => - fieldKey.startsWith(getFieldPath(field, fromIndex)), - ) - .map((fieldKey) => [ - fieldKey as DeepKeys, - formApi.getFieldMeta(fieldKey as DeepKeys), - ]), - ) - - shiftMeta(fields, 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) - } - }) + return { + handleArrayFieldMetaShift, + handleArrayMove, + handleArrayRemove, + handleArraySwap, + handleArrayInsert, + handleArrayFilter, } - - const handleSwapMode = ( - fields: DeepKeys[], - field: DeepKeys, - index: number, - secondIndex: number, - ) => { - fields.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) - }) - } - - return { handleArrayFieldMetaShift } }