Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import useField from "./useField";

function FieldComponent<
FieldValue = any,
T extends HTMLElement = HTMLElement,
T = any,
FormValues = Record<string, any>,
>(
{
Expand Down Expand Up @@ -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<string, any>,
>(
props: FieldProps<FieldValue, T, FormValues> & { ref?: React.Ref<T> },
Expand Down
53 changes: 53 additions & 0 deletions src/renderComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
28 changes: 19 additions & 9 deletions src/renderComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ export default function renderComponent<T>(
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;
Expand All @@ -31,12 +35,15 @@ export default function renderComponent<T>(
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;
Expand All @@ -50,12 +57,15 @@ export default function renderComponent<T>(
}
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);
}
13 changes: 7 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export interface ReactContext<FormValues = Record<string, any>> {
reactFinalForm: FormApi<FormValues>;
}

export interface FieldInputProps<
FieldValue = any,
T extends HTMLElement = HTMLElement,
> {
export interface FieldInputProps<FieldValue = any, T = any> {
name: string;
onBlur: (event?: React.FocusEvent<T>) => void;
onChange: (event: React.ChangeEvent<T> | any) => void;
Expand All @@ -31,7 +28,7 @@ export interface FieldInputProps<

export interface FieldRenderProps<
FieldValue = any,
T extends HTMLElement = HTMLElement,
T = any,
_FormValues = any,
> {
input: FieldInputProps<FieldValue, T>;
Expand All @@ -58,6 +55,10 @@ export interface FieldRenderProps<
};
}

// Backward compatibility: Export FieldMetaState type
export type FieldMetaState<FieldValue = any> =
FieldRenderProps<FieldValue>["meta"];

export interface SubmitEvent {
preventDefault?: () => void;
stopPropagation?: () => void;
Expand Down Expand Up @@ -120,7 +121,7 @@ export interface UseFieldConfig extends UseFieldAutoConfig {

export interface FieldProps<
FieldValue = any,
T extends HTMLElement = HTMLElement,
T = any,
_FormValues = Record<string, any>,
> extends UseFieldConfig,
Omit<RenderableProps<FieldRenderProps<FieldValue, T>>, "children"> {
Expand Down
14 changes: 8 additions & 6 deletions src/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@
const defaultParse = (value: any, _name: string) =>
value === "" ? undefined : value;

const defaultIsEqual = (a: any, b: any): boolean => a === b;

Check warning on line 30 in src/useField.ts

View workflow job for this annotation

GitHub Actions / Lint

'defaultIsEqual' is assigned a value but never used. Allowed unused vars must match /^_/u

function useField<
FieldValue = any,
T extends HTMLElement = HTMLElement,
FormValues = Record<string, any>,
>(name: string, config: UseFieldConfig = {}): FieldRenderProps<FieldValue, T> {
function useField<FieldValue = any, T = any, FormValues = Record<string, any>>(
name: string,
config: UseFieldConfig = {},
): FieldRenderProps<FieldValue, T> {
const {
afterSubmit,
allowNull,
Expand Down Expand Up @@ -117,7 +116,10 @@
// 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;
Expand Down