Skip to content

Commit

Permalink
Expose setErrors and add resetForm (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathan Hodgson authored Apr 16, 2021
1 parent 6ec8e3c commit 83753d9
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ yarn-error.log

# Vercel
.vercel

# JetBrains
.idea
23 changes: 16 additions & 7 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,22 +231,29 @@ function Form(_props) {
setState((state) => {
const newErrors = Object.keys(fields.current).reduce((acc, name) => {
if (typeof errorsMap[name] === "string") {
acc[name] = [errorsMap[name]];
} else if (Array.isArray(errorsMap[name])) {
acc[name] = errorsMap[name];
} else {
acc[name] = state.errors[name];
return setPath(acc, name, [errorsMap[name]]);
}
if (Array.isArray(errorsMap[name])) {
return setPath(acc, name, errorsMap[name]);
}

return acc;
}, {});
}, {...state.errors});

return {
...state,
errors: newErrors,
};
});
}, []);
const resetForm = ({ values, errors } = {}) => {
setState({
values: values ?? initialValues,
errors: errors ?? initialErrors ?? {},
shouldValidateOnChange: false,
namesToValidate: null,
submitStatus: "READY",
});
};
const responsiveFormCSS = useResponsivePropsCSS(props, DEFAULT_PROPS, {
width: responsiveSize("width"),
});
Expand Down Expand Up @@ -303,6 +310,8 @@ function Form(_props) {
validateField,
submitForm,
setValues,
setErrors,
resetForm,
})
: children}
</form>
Expand Down
227 changes: 225 additions & 2 deletions src/components/Form.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable react/prop-types */
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import "@testing-library/jest-dom/extend-expect";
import {
Button,
Expand Down Expand Up @@ -94,6 +94,10 @@ function ComplexForm({
months: "",
},
aboutYourself: "",
address: {
streetNumber: "",
streetName: "",
},
...initialValues,
};

Expand Down Expand Up @@ -145,6 +149,8 @@ function ComplexForm({
label="Tell us about yourself"
height="100"
/>
<Input name="address.streetNumber" label="Street number" />
<Input name="address.streetName" label="Street name" />
<Button type="submit">Submit</Button>
</Grid>
)}
Expand Down Expand Up @@ -177,6 +183,10 @@ describe("Form", () => {
salary: ["Please enter an amount.", "Please select a frequency."],
birthDate: ["Required"],
aboutYourself: ["Required"],
address: {
streetNumber: ["Required"],
streetName: ["Required"],
},
},
values: {
age: {
Expand All @@ -197,6 +207,10 @@ describe("Form", () => {
year: "",
},
aboutYourself: "",
address: {
streetNumber: "",
streetName: "",
},
},
setErrors: expect.any(Function),
})
Expand Down Expand Up @@ -227,6 +241,10 @@ describe("Form", () => {
months: "5",
},
aboutYourself: "I like chess",
address: {
streetNumber: "22",
streetName: "The Esplanade",
},
}}
onSubmit={onSubmit}
unMountFormOnSubmit
Expand All @@ -241,7 +259,7 @@ describe("Form", () => {
});
});

it("setErrors", async () => {
it("sets state correctly when setErrors to be called from onSubmit", async () => {
const onSubmit = jest.fn().mockImplementation(({ setErrors }) => {
setTimeout(() => {
setErrors({
Expand Down Expand Up @@ -272,6 +290,10 @@ describe("Form", () => {
months: "5",
},
aboutYourself: "I like chess",
address: {
streetNumber: "22",
streetName: "The Esplanade",
},
}}
onSubmit={onSubmit}
/>
Expand Down Expand Up @@ -305,6 +327,9 @@ describe("Form", () => {
const initialErrors = {
name: ["This name is already taken."],
aboutYourself: ["You can't use inappropriate words.", "Max 500 words."],
address: {
streetNumber: ["Please enter a street number"],
},
};

render(<ComplexForm initialErrors={initialErrors} />);
Expand All @@ -314,6 +339,9 @@ describe("Form", () => {
screen.getByText("You can't use inappropriate words.")
).toBeInTheDocument();
expect(screen.getByText("Max 500 words.")).toBeInTheDocument();
expect(
screen.getByText("Please enter a street number")
).toBeInTheDocument();
});

it("with hidden fields", async () => {
Expand Down Expand Up @@ -380,4 +408,199 @@ describe("Form", () => {

expect(container.firstChild).toHaveAttribute("data-testid", "my-form");
});

describe("calling exposed functions from render child", () => {
it("sets form state correctly when setErrors is called", async () => {
const initialValues = {
name: "",
age: "",
address: { streetNumber: "", streetName: "" },
};

const initialErrors = {
name: ["Please enter a name"],
age: ["Please enter an age"],
};

const RenderChild = ({ setErrors, state }) => {
useEffect(() => {
if (state.values.name === "Helena") {
setErrors({
name: [
"This name is already taken",
"Try to spell it differently",
],
"address.streetNumber": "Please enter a street number",
});
}
}, [setErrors, state.values.name]);

return (
<>
<Input name="name" label="Name" />
<Input name="age" label="Age" />
<Input name="address.streetNumber" label="Street number" />
<Input name="address.streetName" label="Street name" />
</>
);
};

render(
<Form initialValues={initialValues} initialErrors={initialErrors}>
{RenderChild}
</Form>
);

expect(
screen.queryByText("This name is already taken")
).not.toBeInTheDocument();
expect(
screen.queryByText("Try to spell it differently")
).not.toBeInTheDocument();
expect(screen.queryByText("Please enter a name")).toBeInTheDocument();
expect(screen.queryByText("Please enter an age")).toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).not.toBeInTheDocument();

userEvent.type(screen.getByLabelText("Name"), "Helena");

await waitFor(() => {
expect(
screen.queryByText("This name is already taken")
).toBeInTheDocument();
expect(
screen.queryByText("Try to spell it differently")
).toBeInTheDocument();
expect(screen.queryByText("Please enter a name")).not.toBeInTheDocument();
expect(screen.queryByText("Please enter an age")).toBeInTheDocument();

expect(
screen.queryByText("Please enter a street number")
).toBeInTheDocument();
});
});

it("sets form state to initial values when resetForm is called without argument", async () => {
const initialValues = {
name: "",
address: { streetNumber: "", streetName: "" },
};
const initialErrors = {
name: ["Please enter a name"],
address: { streetNumber: ["Please enter a street number"] },
};

const RenderChild = ({ resetForm }) => (
<>
<Input name="name" label="Name" />
<Input name="address.streetNumber" label="Street number" />
<Input name="address.streetName" label="Street name" />
<Button
testId="resetButton"
onClick={() => {
resetForm();
}}
>
Reset
</Button>
</>
);

render(
<Form initialValues={initialValues} initialErrors={initialErrors}>
{RenderChild}
</Form>
);

expect(screen.queryByText("Please enter a name")).toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("");
expect(screen.getByLabelText("Street name")).toHaveValue("");

userEvent.type(screen.getByLabelText("Name"), "Helena");
userEvent.type(screen.getByLabelText("Street number"), "22");

await waitFor(() => {
expect(
screen.queryByText("Please enter a name")
).not.toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).not.toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("Helena");
expect(screen.getByLabelText("Street number")).toHaveValue("22");
});

userEvent.click(screen.getByTestId("resetButton"));

await waitFor(() => {
expect(screen.queryByText("Please enter a name")).toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("");
expect(screen.getByLabelText("Street name")).toHaveValue("");
});
});

it("sets form state to new values when resetForm is called with argument", async () => {
const initialValues = {
name: "Helena",
address: { streetNumber: "22", streetName: "" },
};
const initialErrors = {};

const newValues = {
name: "Bob",
address: { streetNumber: "1", streetName: "" },
};
const newErrors = {
name: ["Please enter a name"],
address: { streetNumber: ["Please enter a street number"] },
};

const RenderChild = ({ resetForm }) => (
<>
<Input name="name" label="Name" />
<Input name="address.streetNumber" label="Street number" />
<Input name="address.streetName" label="Street name" />
<Button
testId="resetButton"
onClick={() => {
resetForm({ values: newValues, errors: newErrors });
}}
>
Reset
</Button>
</>
);

render(
<Form initialValues={initialValues} initialErrors={initialErrors}>
{RenderChild}
</Form>
);

expect(screen.queryByText("Please enter a name")).not.toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).not.toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("Helena");
expect(screen.getByLabelText("Street number")).toHaveValue("22");

userEvent.click(screen.getByTestId("resetButton"));

await waitFor(() => {
expect(screen.queryByText("Please enter a name")).toBeInTheDocument();
expect(
screen.queryByText("Please enter a street number")
).toBeInTheDocument();
expect(screen.getByLabelText("Name")).toHaveValue("Bob");
expect(screen.getByLabelText("Street number")).toHaveValue("1");
});
});
});
});

1 comment on commit 83753d9

@vercel
Copy link

@vercel vercel bot commented on 83753d9 Apr 16, 2021

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.