diff --git a/src/Field.tsx b/src/Field.tsx index ff034f9..ac95b46 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -5,7 +5,7 @@ import useField from "./useField"; function FieldComponent< FieldValue = any, - T extends HTMLElement = HTMLElement, + T = any, FormValues = Record, >( { @@ -95,7 +95,7 @@ function FieldComponent< // Create a properly typed forwardRef component that preserves generics const Field = React.forwardRef(FieldComponent as any) as < FieldValue = any, - T extends HTMLElement = HTMLElement, + T = any, FormValues = Record, >( props: FieldProps & { ref?: React.Ref }, diff --git a/src/renderComponent.test.js b/src/renderComponent.test.js index 5cf111e..2d99d9e 100644 --- a/src/renderComponent.test.js +++ b/src/renderComponent.test.js @@ -142,3 +142,56 @@ describe("renderComponent", () => { expect(result.props.active).toBe("getter-value"); }); }); + +describe("renderComponent - Issue #1048", () => { + it("should allow rest props to override lazyProps data properties", () => { + const Component = () => null; + const lazyProps = { + input: { + value: "", + onChange: () => {}, + }, + }; + + const customInput = { + value: "foo", + onChange: () => {}, + }; + + const result = renderComponent( + { component: Component, input: customInput }, + lazyProps, + "Field", + ); + + expect(result.type).toBe(Component); + // The custom input prop should override the lazyProps input + expect(result.props.input).toBe(customInput); + expect(result.props.input.value).toBe("foo"); + }); + + it("should still protect getter-only properties from being overwritten", () => { + const Component = () => null; + const lazyProps = { + data: { regular: "value" }, + }; + + // Add a getter-only property + Object.defineProperty(lazyProps, "active", { + get: () => "getter-value", + enumerable: true, + }); + + const result = renderComponent( + { component: Component, active: "override-attempt", custom: "prop" }, + lazyProps, + "Field", + ); + + expect(result.type).toBe(Component); + // Getter-only property should NOT be overridden + expect(result.props.active).toBe("getter-value"); + // Regular data property should be overridable + expect(result.props.custom).toBe("prop"); + }); +}); diff --git a/src/renderComponent.ts b/src/renderComponent.ts index 5544d9d..ed89679 100644 --- a/src/renderComponent.ts +++ b/src/renderComponent.ts @@ -12,14 +12,18 @@ export default function renderComponent( if (component) { // FIX: Don't use Object.assign which tries to overwrite getters // Instead, create a new object with lazyProps descriptors first, - // then add non-conflicting properties from rest + // then add properties from rest (but don't overwrite getter-only properties) const result = {} as any; Object.defineProperties(result, Object.getOwnPropertyDescriptors(lazyProps)); const restDescriptors = Object.getOwnPropertyDescriptors(rest); for (const key in restDescriptors) { - if (!(key in result)) { - Object.defineProperty(result, key, restDescriptors[key]); + const existingDescriptor = Object.getOwnPropertyDescriptor(result, key); + // Skip getter-only properties (these would throw an error if we tried to overwrite them) + if (existingDescriptor && existingDescriptor.get && !existingDescriptor.set) { + continue; } + // For everything else, allow rest to override lazyProps + Object.defineProperty(result, key, restDescriptors[key]); } result.children = children; result.render = render; @@ -31,12 +35,15 @@ export default function renderComponent( result, Object.getOwnPropertyDescriptors(lazyProps), ); - // Only add properties from rest that don't already exist + // Add properties from rest (but don't overwrite getter-only properties) const restDescriptors = Object.getOwnPropertyDescriptors(rest); for (const key in restDescriptors) { - if (!(key in (result as any))) { - Object.defineProperty(result as any, key, restDescriptors[key]); + const existingDescriptor = Object.getOwnPropertyDescriptor(result as any, key); + // Skip getter-only properties + if (existingDescriptor && existingDescriptor.get && !existingDescriptor.set) { + continue; } + Object.defineProperty(result as any, key, restDescriptors[key]); } if (children !== undefined) { (result as any).children = children; @@ -50,12 +57,15 @@ export default function renderComponent( } const result = {} as T; Object.defineProperties(result, Object.getOwnPropertyDescriptors(lazyProps)); - // Only add properties from rest that don't already exist + // Add properties from rest (but don't overwrite getter-only properties) const restDescriptors = Object.getOwnPropertyDescriptors(rest); for (const key in restDescriptors) { - if (!(key in (result as any))) { - Object.defineProperty(result as any, key, restDescriptors[key]); + const existingDescriptor = Object.getOwnPropertyDescriptor(result as any, key); + // Skip getter-only properties + if (existingDescriptor && existingDescriptor.get && !existingDescriptor.set) { + continue; } + Object.defineProperty(result as any, key, restDescriptors[key]); } return children(result); } diff --git a/src/types.ts b/src/types.ts index e09c96f..1100cf3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,10 +15,7 @@ export interface ReactContext> { reactFinalForm: FormApi; } -export interface FieldInputProps< - FieldValue = any, - T extends HTMLElement = HTMLElement, -> { +export interface FieldInputProps { name: string; onBlur: (event?: React.FocusEvent) => void; onChange: (event: React.ChangeEvent | any) => void; @@ -31,7 +28,7 @@ export interface FieldInputProps< export interface FieldRenderProps< FieldValue = any, - T extends HTMLElement = HTMLElement, + T = any, _FormValues = any, > { input: FieldInputProps; @@ -58,6 +55,10 @@ export interface FieldRenderProps< }; } +// Backward compatibility: Export FieldMetaState type +export type FieldMetaState = + FieldRenderProps["meta"]; + export interface SubmitEvent { preventDefault?: () => void; stopPropagation?: () => void; @@ -120,7 +121,7 @@ export interface UseFieldConfig extends UseFieldAutoConfig { export interface FieldProps< FieldValue = any, - T extends HTMLElement = HTMLElement, + T = any, _FormValues = Record, > extends UseFieldConfig, Omit>, "children"> { diff --git a/src/useField.ts b/src/useField.ts index 35a3e47..4dc3e0c 100644 --- a/src/useField.ts +++ b/src/useField.ts @@ -29,11 +29,10 @@ const defaultParse = (value: any, _name: string) => const defaultIsEqual = (a: any, b: any): boolean => a === b; -function useField< - FieldValue = any, - T extends HTMLElement = HTMLElement, - FormValues = Record, ->(name: string, config: UseFieldConfig = {}): FieldRenderProps { +function useField>( + name: string, + config: UseFieldConfig = {}, +): FieldRenderProps { const { afterSubmit, allowNull, @@ -117,7 +116,10 @@ function useField< // If no existing state, create a proper initial state const formState = form.getState(); // Use getIn to support nested field paths like "user.name" or "items[0].id" - const formInitialValue = getIn(formState.initialValues, name); + const formInitialValue = getIn( + formState.initialValues || ({} as FormValues), + name, + ); // Use Form initialValues if available, otherwise use field initialValue let initialStateValue = formInitialValue !== undefined ? formInitialValue : initialValue;