diff --git a/packages/core/components/AutoField/index.spec.tsx b/packages/core/components/AutoField/index.spec.tsx
new file mode 100644
index 0000000000..9c7427f118
--- /dev/null
+++ b/packages/core/components/AutoField/index.spec.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { appStoreContext, createAppStore } from "../../store";
+import { fieldContextStore } from "./store";
+import { AutoFieldPrivate } from ".";
+
+jest.mock("../Sortable", () => ({
+ Sortable: ({
+ children,
+ }: {
+ children: (props: {
+ isDragging: boolean;
+ ref: null;
+ handleRef: null;
+ }) => JSX.Element;
+ }) => children({ isDragging: false, ref: null, handleRef: null }),
+ SortableProvider: ({ children }: { children: JSX.Element }) => children,
+}));
+
+jest.mock("../../bundle", () => {
+ const { setDeep } = jest.requireActual("../../lib/data/set-deep");
+
+ return { setDeep };
+});
+
+describe("AutoField", () => {
+ it("updates controlled custom fields immediately", () => {
+ const appStore = createAppStore();
+ const onChange = jest.fn();
+
+ render(
+
+
+ (
+ onChange(e.currentTarget.value)}
+ />
+ ),
+ }}
+ id="title"
+ name="title"
+ onChange={onChange}
+ />
+
+
+ );
+
+ fireEvent.change(screen.getByRole("textbox"), {
+ target: { value: "Hello brave world" },
+ });
+
+ expect(onChange).toHaveBeenCalledWith("Hello brave world", undefined);
+ expect((screen.getByRole("textbox") as HTMLInputElement).value).toBe(
+ "Hello brave world"
+ );
+ });
+});
diff --git a/packages/core/components/AutoField/index.tsx b/packages/core/components/AutoField/index.tsx
index ba83404bcb..93b094b568 100644
--- a/packages/core/components/AutoField/index.tsx
+++ b/packages/core/components/AutoField/index.tsx
@@ -1,5 +1,5 @@
import getClassNameFactory from "../../lib/get-class-name-factory";
-import { Field, FieldProps } from "../../types";
+import { Field, FieldProps, UiState } from "../../types";
import styles from "./styles.module.css";
import {
@@ -25,6 +25,7 @@ import { useSafeId } from "../../lib/use-safe-id";
import { NestedFieldContext } from "./context";
import { useShallow } from "zustand/react/shallow";
import { getDeep } from "../../lib/data/get-deep";
+import { setDeep } from "../../lib/data/set-deep";
import type {
FieldLabelPropsInternal,
FieldPropsInternalOptional,
@@ -78,6 +79,7 @@ function AutoFieldInternal<
const field = props.field as Field;
const label = field.label;
const labelIcon = field.labelIcon;
+ const fieldStore = useFieldStoreApi();
const defaultId = useSafeId();
const resolvedId = id || defaultId;
@@ -106,6 +108,22 @@ function AutoFieldInternal<
}
});
+ const shouldSyncLocalValue =
+ field.type === "custom" || !!overrides.fieldTypes?.[field.type];
+
+ const onChange = useCallback(
+ (value: any, uiState?: Partial) => {
+ if (shouldSyncLocalValue) {
+ fieldStore.setState(
+ setDeep(fieldStore.getState(), props.name ?? resolvedId, value)
+ );
+ }
+
+ props.onChange(value, uiState);
+ },
+ [fieldStore, props.name, props.onChange, resolvedId, shouldSyncLocalValue]
+ );
+
const mergedProps = useMemo(
() => ({
...props,
@@ -115,8 +133,9 @@ function AutoFieldInternal<
Label,
id: resolvedId,
value: fieldValue,
+ onChange,
}),
- [props, field, label, labelIcon, Label, resolvedId, fieldValue]
+ [props, field, label, labelIcon, Label, resolvedId, fieldValue, onChange]
);
const onFocus = useCallback(