Skip to content

Commit

Permalink
Copy OnChange from react-final-form-listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyomair committed Jun 18, 2024
1 parent d473756 commit 72c01d4
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 54 deletions.
15 changes: 15 additions & 0 deletions .changeset/silly-candles-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@comet/admin": minor
---

Add `OnChange` helper to listen to field changes

**Example**

```tsx
<OnChange name="product">
{(value, previousValue) => {
// Will be called when field 'product' changes
}}
</OnChange>
```
138 changes: 138 additions & 0 deletions packages/admin/admin/src/form/helpers/OnChange.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copied from https://github.com/final-form/react-final-form-listeners/blob/master/src/OnChange.test.js
import { cleanup, fireEvent, render } from "@testing-library/react";
import React from "react";
import { Field, Form } from "react-final-form";

import { OnChange } from "./OnChange";

const onSubmitMock = () => {
// Noop
};

describe("OnChange", () => {
afterEach(cleanup);

it("should not call listener on first render", () => {
const spy = jest.fn();
render(
<Form onSubmit={onSubmitMock} initialValues={{ foo: "bar" }}>
{() => <OnChange name="foo">{spy}</OnChange>}
</Form>,
);
expect(spy).not.toHaveBeenCalled();
});

it("should call listener when going from uninitialized to value", () => {
const spy = jest.fn();
const { getByTestId } = render(
<Form onSubmit={onSubmitMock}>
{() => (
<form>
<Field name="name" component="input" data-testid="name" />
<OnChange name="name">{spy}</OnChange>
</form>
)}
</Form>,
);
expect(spy).not.toHaveBeenCalled();
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith("erikras", "");
});

it("should call listener when going from initialized to value", () => {
const spy = jest.fn();
const { getByTestId } = render(
<Form onSubmit={onSubmitMock} initialValues={{ name: "erik" }}>
{() => (
<form>
<Field name="name" component="input" data-testid="name" />
<OnChange name="name">{spy}</OnChange>
</form>
)}
</Form>,
);
expect(spy).not.toHaveBeenCalled();
fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith("erikras", "erik");
});

it("should call listener when changing to null", () => {
const spy = jest.fn();
const { getByTestId } = render(
<Form onSubmit={onSubmitMock} initialValues={{ name: "erikras" }}>
{() => (
<form>
<Field name="name" component="input" data-testid="name" />
<OnChange name="name">{spy}</OnChange>
</form>
)}
</Form>,
);
expect(spy).not.toHaveBeenCalled();
fireEvent.change(getByTestId("name"), { target: { value: null } });
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith("", "erikras");
});

it("should handle quick subsequent changes properly", () => {
const toppings = ["Pepperoni", "Mushrooms", "Olives"];
const { getByTestId } = render(
<Form onSubmit={onSubmitMock}>
{({ form }) => (
<React.Fragment>
<Field name="everything" component="input" type="checkbox" data-testid="everything" />
<OnChange name="everything">
{(next) => {
if (next) {
return form.change("toppings", toppings);
}
}}
</OnChange>
{toppings.length > 0 &&
toppings.map((topping, index) => {
return (
<Field component="input" key={topping} name="toppings" value={topping} type="checkbox" data-testid={topping} />
);
})}
<OnChange name="toppings">
{(next) => {
form.change("everything", next && next.length === toppings.length);
}}
</OnChange>
</React.Fragment>
)}
</Form>,
);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(false);
expect((getByTestId("Pepperoni") as HTMLInputElement).checked).toBe(false);
expect((getByTestId("Mushrooms") as HTMLInputElement).checked).toBe(false);
expect((getByTestId("Olives") as HTMLInputElement).checked).toBe(false);

fireEvent.click(getByTestId("Pepperoni"));
expect((getByTestId("Pepperoni") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(false);

fireEvent.click(getByTestId("Mushrooms"));
expect((getByTestId("Mushrooms") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(false);

fireEvent.click(getByTestId("Olives"));
expect((getByTestId("Olives") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(true);

fireEvent.click(getByTestId("Olives"));
expect((getByTestId("Olives") as HTMLInputElement).checked).toBe(false);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(false);

fireEvent.click(getByTestId("everything"));
expect((getByTestId("Pepperoni") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("Mushrooms") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("Olives") as HTMLInputElement).checked).toBe(true);
expect((getByTestId("everything") as HTMLInputElement).checked).toBe(true);
});
});
21 changes: 21 additions & 0 deletions packages/admin/admin/src/form/helpers/OnChange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Inspired by https://github.com/final-form/react-final-form-listeners/blob/master/src/OnChange.js
import { useEffect, useRef } from "react";
import { useField } from "react-final-form";

type Props = { name: string; children: (value: any, previousValue: any) => void };

function OnChange({ name, children }: Props) {
const { input } = useField(name);
const previousValue = useRef(input.value);

useEffect(() => {
if (input.value !== previousValue.current) {
children(input.value, previousValue.current);
previousValue.current = input.value;
}
}, [input.value, children]);

return null;
}

export { OnChange };
1 change: 1 addition & 0 deletions packages/admin/admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export { FinalFormRangeInput, FinalFormRangeInputClassKey, FinalFormRangeInputPr
export { FinalFormSearchTextField, FinalFormSearchTextFieldProps } from "./form/FinalFormSearchTextField";
export { FinalFormSelect, FinalFormSelectProps } from "./form/FinalFormSelect";
export { FormSection, FormSectionClassKey, FormSectionProps } from "./form/FormSection";
export { OnChange } from "./form/helpers/OnChange";
export { FinalFormRadio, FinalFormRadioProps } from "./form/Radio";
export { FinalFormSwitch, FinalFormSwitchProps } from "./form/Switch";
export { FormMutation } from "./FormMutation";
Expand Down
51 changes: 28 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
"react": "^17.0",
"react-dom": "^17.0.2",
"react-final-form": "^6.3.1",
"react-final-form-listeners": "^1.0.0",
"react-intl": "^6.0.0",
"react-select": "^3.0.4",
"require-from-string": "^2.0.2",
Expand Down
Loading

0 comments on commit 72c01d4

Please sign in to comment.