Skip to content

Commit

Permalink
馃獰 馃敡 Add reinitializeDefaultValues prop Form (#6171)
Browse files Browse the repository at this point in the history
Co-authored-by: tealjulia <larson.teal@gmail.com>
  • Loading branch information
josephkmh and teallarson committed May 4, 2023
1 parent 6309e71 commit 2d93ff1
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 13 deletions.
105 changes: 93 additions & 12 deletions airbyte-webapp/src/components/forms/Form.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import * as yup from "yup";
import { SchemaOf } from "yup";

Expand All @@ -24,6 +25,18 @@ const mockDefaultValues: MockFormValues = {
lastName: "",
};

const FIRST_NAME = {
label: "First Name",
name: "firstName",
fieldType: "input",
};

const LAST_NAME = {
label: "Last Name",
name: "lastName",
fieldType: "input",
};

describe(`${Form.name}`, () => {
it("should call onSubmit upon submission", async () => {
const mockOnSubmit = jest.fn();
Expand All @@ -33,14 +46,14 @@ describe(`${Form.name}`, () => {
defaultValues={mockDefaultValues}
onSubmit={(values) => Promise.resolve(mockOnSubmit(values))}
>
<FormControl name="firstName" label="First Name" fieldType="input" />
<FormControl name="lastName" label="Last Name" fieldType="input" />
<FormControl name={FIRST_NAME.name} label={FIRST_NAME.label} fieldType="input" />
<FormControl name={LAST_NAME.name} label={LAST_NAME.label} fieldType="input" />
<FormSubmissionButtons />
</Form>
);

userEvent.type(screen.getByLabelText("First Name"), "John");
userEvent.type(screen.getByLabelText("Last Name"), "Doe");
userEvent.type(screen.getByLabelText(FIRST_NAME.label), "John");
userEvent.type(screen.getByLabelText(LAST_NAME.label), "Doe");
const submitButton = screen.getAllByRole("button").filter((button) => button.getAttribute("type") === "submit")[0];
await waitFor(() => expect(submitButton).toBeEnabled());
userEvent.click(submitButton);
Expand All @@ -57,14 +70,14 @@ describe(`${Form.name}`, () => {
onSubmit={() => Promise.resolve()}
onSuccess={mockOnSuccess}
>
<FormControl name="firstName" label="First Name" fieldType="input" />
<FormControl name="lastName" label="Last Name" fieldType="input" />
<FormControl name="firstName" label={FIRST_NAME.label} fieldType="input" />
<FormControl name="lastName" label={LAST_NAME.label} fieldType="input" />
<FormSubmissionButtons />
</Form>
);

userEvent.type(screen.getByLabelText("First Name"), "John");
userEvent.type(screen.getByLabelText("Last Name"), "Doe");
userEvent.type(screen.getByLabelText(FIRST_NAME.label), "John");
userEvent.type(screen.getByLabelText(LAST_NAME.label), "Doe");
const submitButton = screen.getAllByRole("button").filter((button) => button.getAttribute("type") === "submit")[0];
await waitFor(() => expect(submitButton).toBeEnabled());
userEvent.click(submitButton);
Expand All @@ -82,18 +95,86 @@ describe(`${Form.name}`, () => {
onSubmit={() => Promise.reject()}
onError={mockOnError}
>
<FormControl name="firstName" label="First Name" fieldType="input" />
<FormControl name="lastName" label="Last Name" fieldType="input" />
<FormControl name={FIRST_NAME.name} label={FIRST_NAME.label} fieldType="input" />
<FormControl name={LAST_NAME.name} label={LAST_NAME.label} fieldType="input" />
<FormSubmissionButtons />
</Form>
);

userEvent.type(screen.getByLabelText("First Name"), "John");
userEvent.type(screen.getByLabelText("Last Name"), "Doe");
userEvent.type(screen.getByLabelText(FIRST_NAME.label), "John");
userEvent.type(screen.getByLabelText(LAST_NAME.label), "Doe");
const submitButton = screen.getAllByRole("button").filter((button) => button.getAttribute("type") === "submit")[0];
await waitFor(() => expect(submitButton).toBeEnabled());
userEvent.click(submitButton);

await waitFor(() => expect(mockOnError).toHaveBeenCalledTimes(1));
});

describe("reinitializeDefaultValues", () => {
const mockStartDefaultValues = {
firstName: "John",
lastName: "Doe",
};

const mockEndDefaultValues = {
firstName: "Jane",
lastName: "Smith",
};

const CHANGE_DEFAULT_VALUES_BUTTON_TEXT = "Change default values";

// A simple wrapper that will change the default values of the form
const MockReinitializeForm = ({ reinitializeDefaultValues }: { reinitializeDefaultValues?: boolean }) => {
const [defaultValues, setDefaultValues] = React.useState(mockStartDefaultValues);

return (
<>
<Form
schema={mockSchema}
defaultValues={defaultValues}
onSubmit={() => Promise.resolve()}
reinitializeDefaultValues={reinitializeDefaultValues}
>
<FormControl name={FIRST_NAME.name} label={FIRST_NAME.label} fieldType="input" />
<FormControl name={LAST_NAME.name} label={LAST_NAME.label} fieldType="input" />
<FormSubmissionButtons />
</Form>
<button onClick={() => setDefaultValues(mockEndDefaultValues)} type="button">
{CHANGE_DEFAULT_VALUES_BUTTON_TEXT}
</button>
</>
);
};

it("does not reinitialize default values by default", async () => {
await render(<MockReinitializeForm />);

userEvent.click(screen.getByText(CHANGE_DEFAULT_VALUES_BUTTON_TEXT));

await waitFor(() =>
expect(screen.getByLabelText(FIRST_NAME.label)).toHaveValue(mockStartDefaultValues.firstName)
);
});

it("reinitializes default values when reinitializeDefaultValues is true", async () => {
await render(<MockReinitializeForm reinitializeDefaultValues />);

userEvent.click(screen.getByText(CHANGE_DEFAULT_VALUES_BUTTON_TEXT));

await waitFor(() => expect(screen.getByLabelText(FIRST_NAME.label)).toHaveValue(mockEndDefaultValues.firstName));
});

it("does not reinitialize default values if the form is dirty", async () => {
const NEW_FIRST_NAME = "Susan";

await render(<MockReinitializeForm reinitializeDefaultValues />);

userEvent.click(screen.getByText(CHANGE_DEFAULT_VALUES_BUTTON_TEXT));

// By making the form dirty, reinitialization of default values should not occur
userEvent.type(screen.getByLabelText(FIRST_NAME.label), `{selectall}${NEW_FIRST_NAME}`);

await waitFor(() => expect(screen.getByLabelText(FIRST_NAME.label)).toHaveValue(NEW_FIRST_NAME));
});
});
});
13 changes: 12 additions & 1 deletion airbyte-webapp/src/components/forms/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { ReactNode } from "react";
import { ReactNode, useEffect } from "react";
import { useForm, FormProvider, DeepPartial, useFormState } from "react-hook-form";
import { SchemaOf } from "yup";

Expand All @@ -16,6 +16,10 @@ interface FormProps<T extends FormValues> {
defaultValues: DeepPartial<T>;
children?: ReactNode | undefined;
trackDirtyChanges?: boolean;
/**
* Reinitialize form values when defaultValues changes. This will only work if the form is not dirty. Defaults to false.
*/
reinitializeDefaultValues?: boolean;
}

const HookFormDirtyTracker = () => {
Expand All @@ -31,13 +35,20 @@ export const Form = <T extends FormValues>({
defaultValues,
schema,
trackDirtyChanges = false,
reinitializeDefaultValues = false,
}: FormProps<T>) => {
const methods = useForm<T>({
defaultValues,
resolver: yupResolver(schema),
mode: "onChange",
});

useEffect(() => {
if (reinitializeDefaultValues && !methods.formState.isDirty) {
methods.reset(defaultValues);
}
}, [reinitializeDefaultValues, defaultValues, methods]);

const processSubmission = (values: T) =>
onSubmit(values)
.then(() => {
Expand Down

0 comments on commit 2d93ff1

Please sign in to comment.