Skip to content

Commit

Permalink
Merge pull request #131 from watershed-climate/sterling-07-19-fix_Cus…
Browse files Browse the repository at this point in the history
…tomChildComponent_remount_focus_issue

fix CustomChildComponent remount
  • Loading branch information
scamden authored Jul 20, 2023
2 parents e757493 + a4653eb commit 1ab3e96
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 9 deletions.
94 changes: 91 additions & 3 deletions src/__tests__/createSchemaForm.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,6 +7,7 @@ import {
TestCustomFieldSchema,
TestForm,
TestFormWithSubmit,
TextField,
textFieldTestId,
} from "./utils/testForm";
import {
Expand All @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 (
<form>
{children}
{isSubmitting}
</form>
);
},
});

const TestComponent = () => {
const form = useForm<z.infer<typeof schema>>({
mode: "onChange",
resolver: zodResolver(schema),
});
const values = {
...form.getValues(),
...useWatch({ control: form.control }),
};

return (
<Form
form={form}
schema={schema}
defaultValues={{}}
props={{
fieldOne: {
testId: "fieldOne",
beforeElement: <>Moo{JSON.stringify(values)}</>,
},
fieldTwo: { testId: "fieldTwo" },
}}
onSubmit={() => {}}
>
{(fields) => {
const { isDirty } = useFormState();
const [state, setState] = useState(0);
useEffect(() => {
setState(1);
}, []);
return (
<>
{Object.values(fields)}
<div data-testid="dirty">{JSON.stringify(isDirty)}</div>
<div data-testid="state">{state}</div>
</>
);
}}
</Form>
);
};
render(<TestComponent />);
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();
});
});
});
30 changes: 24 additions & 6 deletions src/createSchemaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -563,21 +563,39 @@ export function createTsForm<
}

const renderedFields = renderFields(schema, props);
const renderedFieldNodes = flattenRenderedElements(renderedFields);
return (
<FormProvider {..._form}>
<ActualFormComponent {...formProps} onSubmit={submitFn}>
{renderBefore && renderBefore({ submit: submitFn })}
{CustomChildrenComponent ? (
<CustomChildrenComponent {...renderedFields} />
) : (
renderedFieldNodes
)}
<Children
renderedFields={renderedFields}
CustomChildrenComponent={CustomChildrenComponent}
/>

{renderAfter && renderAfter({ submit: submitFn })}
</ActualFormComponent>
</FormProvider>
);
};

// 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<SchemaType extends RTFFormSchemaType>({
CustomChildrenComponent,
renderedFields,
}: {
renderedFields: RenderedFieldMap<SchemaType>;
CustomChildrenComponent?: FunctionComponent<RenderedFieldMap<SchemaType>>;
}) {
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)
Expand Down

1 comment on commit 1ab3e96

@vercel
Copy link

@vercel vercel bot commented on 1ab3e96 Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.