From 3280c9cb77a8fffa86d83b1b7266eeb86cfe1ca0 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Thu, 7 May 2026 04:07:49 +0200 Subject: [PATCH] Fix FieldArray active and touched meta --- src/FieldArray.test.tsx | 48 ++++++++++++++++++++++++++++++++++++++++ src/useFieldArray.ts | 49 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/FieldArray.test.tsx b/src/FieldArray.test.tsx index da80be9..e68fc62 100644 --- a/src/FieldArray.test.tsx +++ b/src/FieldArray.test.tsx @@ -722,6 +722,54 @@ describe('FieldArray', () => { expect(queryByTestId('child')).not.toBe(null) }) + it('should aggregate subscribed active and touched meta from array fields', () => { + const { getByTestId } = render( +
+ {() => ( + + + {({ fields, meta }) => ( +
+
+ {meta.active ? 'Active' : 'Inactive'} +
+
+ {meta.touched ? 'Touched' : 'Untouched'} +
+ {fields.map((field) => ( + + {({ input }) => ( + + )} + + ))} +
+ )} +
+
+ )} + + ) + + expect(getByTestId('arrayActive')).toHaveTextContent('Inactive') + expect(getByTestId('arrayTouched')).toHaveTextContent('Untouched') + + fireEvent.focus(getByTestId('names[0].input')) + + expect(getByTestId('arrayActive')).toHaveTextContent('Active') + expect(getByTestId('arrayTouched')).toHaveTextContent('Untouched') + + fireEvent.blur(getByTestId('names[0].input')) + + expect(getByTestId('arrayActive')).toHaveTextContent('Inactive') + expect(getByTestId('arrayTouched')).toHaveTextContent('Touched') + }) + it('should provide default isEqual that does shallow compare of items', () => { const { getByTestId } = render(
+ fieldName === arrayName || fieldName.startsWith(`${arrayName}[`) + const useFieldArray = ( name: string, { @@ -83,13 +86,51 @@ const useFieldArray = ( format: v => v }) + const needsAggregateActive = !!subscription.active + const needsAggregateTouched = !!subscription.touched + const formState = useFormState({ + subscription: { + active: needsAggregateActive, + touched: needsAggregateTouched + } + }) + // FIX #167: Don't destructure/spread meta object because it has lazy getters // Extract length directly from meta when needed const { meta, input } = fieldState const length = meta.length // Create a new meta object that excludes length, preserving lazy getters - const metaWithoutLength = copyPropertyDescriptors(meta, {} as any, ['length']) + const metaWithoutLength = copyPropertyDescriptors( + meta, + {} as any, + [ + 'length', + ...(needsAggregateActive ? ['active'] : []), + ...(needsAggregateTouched ? ['touched'] : []) + ] + ) + + if (needsAggregateActive) { + Object.defineProperty(metaWithoutLength, 'active', { + enumerable: true, + get: () => + typeof formState.active === 'string' && isArrayField(name, formState.active) + }) + } + + if (needsAggregateTouched) { + Object.defineProperty(metaWithoutLength, 'touched', { + enumerable: true, + get: () => { + const touched = formState.touched + return !!touched && + Object.keys(touched).some( + (fieldName) => !!touched[fieldName] && isArrayField(name, fieldName) + ) + } + }) + } const forEach = (iterator: (name: string, index: number) => void): void => { // required || for Flow, but results in uncovered line in Jest/Istanbul