From b1cfc6aa7092a26fde3ac27243c1d9985898f592 Mon Sep 17 00:00:00 2001 From: Sterling Camden Date: Wed, 19 Jul 2023 11:07:59 -0700 Subject: [PATCH 1/2] fix CustomChildComponent remount --- src/__tests__/createSchemaForm.test.tsx | 94 ++++++++++++++++++++++++- src/createSchemaForm.tsx | 8 +-- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/__tests__/createSchemaForm.test.tsx b/src/__tests__/createSchemaForm.test.tsx index 3df890b..579eb62 100644 --- a/src/__tests__/createSchemaForm.test.tsx +++ b/src/__tests__/createSchemaForm.test.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { z } from "zod"; import { render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -7,6 +7,7 @@ import { TestCustomFieldSchema, TestForm, TestFormWithSubmit, + TextField, textFieldTestId, } from "./utils/testForm"; import { @@ -18,7 +19,13 @@ import { SPLIT_DESCRIPTION_SYMBOL as DESCRIPTION_SEPARATOR_SYMBOL, SPLIT_DESCRIPTION_SYMBOL, } from "../getMetaInformationForZodType"; -import { Control, useController, useForm } from "react-hook-form"; +import { + Control, + useController, + useForm, + useFormState, + useWatch, +} from "react-hook-form"; import userEvent from "@testing-library/user-event"; import { useDescription, @@ -31,6 +38,7 @@ import { } from "../FieldContext"; import { expectTypeOf } from "expect-type"; import { createUniqueFieldSchema } from "../createFieldSchema"; +import { zodResolver } from "@hookform/resolvers/zod"; const testIds = { textField: "_text-field", @@ -1479,7 +1487,7 @@ describe("createSchemaForm", () => { return createUniqueFieldSchema(z.date().min(min).max(max), uniqueId); }, get component() { - const { min,max, label, uniqueId } = this; + const { min, max, label, uniqueId } = this; const ArrayDateFieldComponent = () => { const fieldInfo = useDateFieldInfo(); @@ -1847,4 +1855,84 @@ describe("createSchemaForm", () => { const inputs = screen.getAllByTestId(/dynamic-array-input/); expect(inputs.length).toBe(3); }); + describe("CustomChildComponent", () => { + it("should not drop focus on rerender", async () => { + const schema = z.object({ + fieldOne: z.string().regex(/moo/), + fieldTwo: z.string(), + }); + + const Form = createTsForm([[z.string(), TextField]] as const, { + FormComponent: ({ + children, + }: { + onSubmit: () => void; + children: ReactNode; + }) => { + const { isSubmitting } = useFormState(); + return ( +
+ {children} + {isSubmitting} +
+ ); + }, + }); + + const TestComponent = () => { + const form = useForm>({ + mode: "onChange", + resolver: zodResolver(schema), + }); + const values = { + ...form.getValues(), + ...useWatch({ control: form.control }), + }; + + return ( +
Moo{JSON.stringify(values)}, + }, + fieldTwo: { testId: "fieldTwo" }, + }} + onSubmit={() => {}} + > + {(fields) => { + const { isDirty } = useFormState(); + const [state, setState] = useState(0); + useEffect(() => { + setState(1); + }, []); + return ( + <> + {Object.values(fields)} +
{JSON.stringify(isDirty)}
+
{state}
+ + ); + }} +
+ ); + }; + render(); + const fieldOne = screen.queryByTestId("fieldOne"); + if (!fieldOne) throw new Error("fieldOne not found"); + fieldOne.focus(); + expect(fieldOne).toHaveFocus(); + await userEvent.type(fieldOne, "t"); + expect(fieldOne).toHaveFocus(); + await userEvent.type(fieldOne, "2"); + expect(fieldOne).toHaveFocus(); + // verify that context and stateful hooks still work + expect(screen.queryByTestId("dirty")).toHaveTextContent("true"); + expect(screen.queryByTestId("state")).toHaveTextContent("1"); + screen.debug(); + }); + }); }); diff --git a/src/createSchemaForm.tsx b/src/createSchemaForm.tsx index 8adbad5..b395f98 100644 --- a/src/createSchemaForm.tsx +++ b/src/createSchemaForm.tsx @@ -568,11 +568,9 @@ export function createTsForm< {renderBefore && renderBefore({ submit: submitFn })} - {CustomChildrenComponent ? ( - - ) : ( - renderedFieldNodes - )} + {CustomChildrenComponent + ? CustomChildrenComponent(renderedFields) + : renderedFieldNodes} {renderAfter && renderAfter({ submit: submitFn })} From a4653eb9e705cc8a55c3935ac934fd4d59b9c8a7 Mon Sep 17 00:00:00 2001 From: Sterling Camden Date: Wed, 19 Jul 2023 16:13:25 -0700 Subject: [PATCH 2/2] have to have a wrapping component to allow context to work --- src/createSchemaForm.tsx | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/createSchemaForm.tsx b/src/createSchemaForm.tsx index b395f98..30d39ee 100644 --- a/src/createSchemaForm.tsx +++ b/src/createSchemaForm.tsx @@ -563,19 +563,39 @@ export function createTsForm< } const renderedFields = renderFields(schema, props); - const renderedFieldNodes = flattenRenderedElements(renderedFields); return ( {renderBefore && renderBefore({ submit: submitFn })} - {CustomChildrenComponent - ? CustomChildrenComponent(renderedFields) - : renderedFieldNodes} + + {renderAfter && renderAfter({ submit: submitFn })} ); }; + + // these needs to at least have one component wrapping it or the context won't propogate + // i believe that means any hooks used in the CustomChildrenComponent are really tied to the lifecycle of this Children component... 😬 + // i ~think~ that's ok + function Children({ + CustomChildrenComponent, + renderedFields, + }: { + renderedFields: RenderedFieldMap; + CustomChildrenComponent?: FunctionComponent>; + }) { + return ( + <> + {CustomChildrenComponent + ? CustomChildrenComponent(renderedFields) + : flattenRenderedElements(renderedFields)} + + ); + } } // handles internal custom submit logic // Implements a workaround to allow devs to set form values to undefined (as it breaks react hook form)