---
__tests__/FormSyncValidation.spec.js | 15 +-
__tests__/Input.spec.js | 97 +++++++++++-
__tests__/PersistStateOnUnmount.spec.js | 120 +++++++++++++++
__tests__/helpers/components/CheckBox.jsx | 21 +++
.../components/CollectionDynamicAdded.jsx | 2 +-
.../FormWithValidationAfterMounted.jsx | 29 ++++
.../PersistStateOnUnmountHelpers.jsx | 138 ++++++++++++++++++
__tests__/helpers/components/Radio.jsx | 21 +++
docs/PersistStateOnUnmount.mdx | 42 ++++++
examples/helpers/SimpleForm.js | 1 -
package-lock.json | 2 +-
package.json | 2 +-
src/PersistStateOnUnmount.js | 11 ++
src/hooks/useField.js | 17 ++-
src/hooks/useForm.js | 8 +
src/hooks/useObject.js | 2 +-
src/index.d.ts | 1 +
src/index.js | 1 +
18 files changed, 520 insertions(+), 10 deletions(-)
create mode 100644 __tests__/PersistStateOnUnmount.spec.js
create mode 100644 __tests__/helpers/components/CheckBox.jsx
create mode 100644 __tests__/helpers/components/FormWithValidationAfterMounted.jsx
create mode 100644 __tests__/helpers/components/PersistStateOnUnmountHelpers.jsx
create mode 100644 __tests__/helpers/components/Radio.jsx
create mode 100644 docs/PersistStateOnUnmount.mdx
create mode 100644 src/PersistStateOnUnmount.js
diff --git a/__tests__/FormSyncValidation.spec.js b/__tests__/FormSyncValidation.spec.js
index 6514349..3dadcef 100644
--- a/__tests__/FormSyncValidation.spec.js
+++ b/__tests__/FormSyncValidation.spec.js
@@ -1,6 +1,7 @@
import React from "react";
-import { cleanup, fireEvent, act } from "@testing-library/react";
+import { cleanup, fireEvent, act, render } from "@testing-library/react";
+import { FormWithValidationAfterMounted } from "./helpers/components/FormWithValidationAfterMounted";
import Reset from "./helpers/components/Reset";
import { mountForm } from "./helpers/utils/mountForm";
@@ -38,6 +39,18 @@ describe("Component => Form (sync validation)", () => {
expect(errorLabel.innerHTML).toBe("Mail not Valid");
});
+ it("should synchronously validate a Form if Fields are added after Form is mounted", () => {
+ const { getByTestId } = render();
+ const addBtn = getByTestId("addBtn");
+
+ act(() => {
+ fireEvent.click(addBtn);
+ });
+
+ const isValid = getByTestId("isValid");
+ expect(isValid.innerHTML).toBe("false");
+ });
+
it("should synchronously validate a Form with touched prop true", () => {
const name = "email";
const value = "bebo@test.it";
diff --git a/__tests__/Input.spec.js b/__tests__/Input.spec.js
index 206b60c..a135f9b 100644
--- a/__tests__/Input.spec.js
+++ b/__tests__/Input.spec.js
@@ -9,6 +9,8 @@ import {
import userEvent from "@testing-library/user-event";
import { Input } from "./../src";
import InputAsync from "./helpers/components/InputAsync";
+import Radio from "./helpers/components/Radio";
+import CheckBox from "./helpers/components/CheckBox";
import InputSyncValidation from "./helpers/components/InputSyncValidation";
import Submit from "./helpers/components/Submit";
import Reset from "./helpers/components/Reset";
@@ -396,7 +398,22 @@ describe("Component => Input", () => {
input.blur();
});
- const errorLabel = getByTestId("errorLabel");
+ let errorLabel = getByTestId("errorLabel");
+ expect(errorLabel).toBeDefined();
+
+ act(() => {
+ input.focus();
+ fireEvent.change(input, { target: { value: "1234" } });
+ });
+
+ expect(() => getByTestId("errorLabel")).toThrow();
+
+ act(() => {
+ input.focus();
+ fireEvent.change(input, { target: { value: "" } });
+ });
+
+ errorLabel = getByTestId("errorLabel");
expect(errorLabel).toBeDefined();
const reset = getByTestId("reset");
@@ -463,6 +480,84 @@ describe("Component => Input", () => {
expect(() => getByTestId("errorLabel")).toThrow();
});
+ it("should use sync validator functions to validate a Radio input", () => {
+ const props = { onReset, onChange };
+ const children = [
+ ,
+ ,
+
+ ];
+
+ const { getByTestId } = mountForm({ children, props });
+
+ const submit = getByTestId("submit");
+ const reset = getByTestId("reset");
+ const radio = getByTestId("radio");
+
+ act(() => {
+ fireEvent.click(submit);
+ });
+
+ let errorLabel = getByTestId("errorLabel");
+ expect(errorLabel).toBeDefined();
+
+ act(() => {
+ radio.focus();
+ fireEvent.click(radio);
+ });
+
+ expect(() => getByTestId("errorLabel")).toThrow();
+
+ act(() => {
+ fireEvent.click(reset);
+ });
+ expect(onReset).toHaveBeenCalledWith({}, false);
+ expect(() => getByTestId("errorLabel")).toThrow();
+ });
+
+ it("should use sync validator functions to validate a Checkbox input", () => {
+ const props = { onReset, onChange };
+ const children = [
+ ,
+ ,
+
+ ];
+
+ const { getByTestId } = mountForm({ children, props });
+
+ const submit = getByTestId("submit");
+ const reset = getByTestId("reset");
+ const checkBox = getByTestId("checkbox");
+
+ act(() => {
+ fireEvent.click(submit);
+ });
+
+ let errorLabel = getByTestId("errorLabel");
+ expect(errorLabel).toBeDefined();
+
+ act(() => {
+ checkBox.focus();
+ fireEvent.click(checkBox);
+ });
+
+ expect(() => getByTestId("errorLabel")).toThrow();
+
+ act(() => {
+ checkBox.focus();
+ fireEvent.click(checkBox);
+ });
+
+ errorLabel = getByTestId("errorLabel");
+ expect(errorLabel).toBeDefined();
+
+ act(() => {
+ fireEvent.click(reset);
+ });
+ expect(onReset).toHaveBeenCalledWith({}, false);
+ expect(() => getByTestId("errorLabel")).toThrow();
+ });
+
it("should use an async validator function to validate the Input", async () => {
const value = "33";
const name = "test";
diff --git a/__tests__/PersistStateOnUnmount.spec.js b/__tests__/PersistStateOnUnmount.spec.js
new file mode 100644
index 0000000..df3ade6
--- /dev/null
+++ b/__tests__/PersistStateOnUnmount.spec.js
@@ -0,0 +1,120 @@
+import React from "react";
+import { cleanup, fireEvent, act, render } from "@testing-library/react";
+
+import {
+ PersistStateOnUnmountHelpers,
+ initialState
+} from "./helpers/components/PersistStateOnUnmountHelpers";
+
+const onInit = jest.fn();
+const onChange = jest.fn();
+const onReset = jest.fn();
+const onSubmit = jest.fn();
+
+afterEach(cleanup);
+
+describe("Component => PersistStateOnUnmount", () => {
+ beforeEach(() => {
+ onInit.mockClear();
+ onChange.mockClear();
+ onReset.mockClear();
+ onSubmit.mockClear();
+ });
+
+ it("should Collection state persist on unmonuted", () => {
+ const stateExpected = {
+ ...initialState,
+ user: { ...initialState.user, lastname: "hero" }
+ };
+ const props = { onInit, onChange };
+ const { getByTestId } = render();
+
+ const toggleCollection = getByTestId("toggleCollection");
+ const lastName = getByTestId("lastname");
+ expect(onInit).toHaveBeenCalledWith(initialState, true);
+
+ act(() => {
+ fireEvent.change(lastName, { target: { value: "hero" } });
+ });
+
+ act(() => {
+ fireEvent.click(toggleCollection);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(stateExpected, true);
+
+ const togglekeepValue = getByTestId("togglekeepValue");
+
+ act(() => {
+ fireEvent.click(togglekeepValue);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(stateExpected, true);
+ });
+
+ it("should Checkbox state persist on unmonuted", () => {
+ const stateExpected = { ...initialState, other: [, "3"] };
+ const props = { onInit, onChange };
+ const { getByTestId } = render();
+
+ const toggleNestedInput = getByTestId("toggleNestedInput");
+ const other1 = getByTestId("other1");
+ expect(onInit).toHaveBeenCalledWith(initialState, true);
+
+ act(() => {
+ fireEvent.click(other1);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(stateExpected, true);
+
+ act(() => {
+ fireEvent.click(other1);
+ });
+
+ act(() => {
+ fireEvent.click(toggleNestedInput);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(initialState, true);
+ });
+
+ it("should Radio state persist on unmonuted", () => {
+ const stateExpected = { ...initialState, gender: "M" };
+ const props = { onInit, onChange };
+ const { getByTestId } = render();
+
+ const toggleNestedRadio = getByTestId("toggleNestedRadio");
+ const genderm = getByTestId("genderm");
+ expect(onInit).toHaveBeenCalledWith(initialState, true);
+
+ act(() => {
+ fireEvent.click(genderm);
+ });
+
+ act(() => {
+ fireEvent.click(toggleNestedRadio);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(stateExpected, true);
+ });
+
+ it("should Select state persist on unmonuted", () => {
+ const stateExpected = { ...initialState, select: "2" };
+ const props = { onInit, onChange };
+ const { getByTestId } = render();
+
+ const toggleSelect = getByTestId("toggleSelect");
+ const select = getByTestId("select");
+ expect(onInit).toHaveBeenCalledWith(initialState, true);
+
+ act(() => {
+ fireEvent.change(select, { target: { value: "2" } });
+ });
+
+ act(() => {
+ fireEvent.click(toggleSelect);
+ });
+
+ expect(onChange).toHaveBeenCalledWith(stateExpected, true);
+ });
+});
diff --git a/__tests__/helpers/components/CheckBox.jsx b/__tests__/helpers/components/CheckBox.jsx
new file mode 100644
index 0000000..27dd27c
--- /dev/null
+++ b/__tests__/helpers/components/CheckBox.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { Input, useValidation } from "./../../../src";
+
+const required = value => (value ? undefined : "Required");
+
+export default function CheckBox({ name = "checkbox", value }) {
+ const [status, validation] = useValidation([required]);
+ return (
+
+
+
+ {status.error && }
+
+ );
+}
diff --git a/__tests__/helpers/components/CollectionDynamicAdded.jsx b/__tests__/helpers/components/CollectionDynamicAdded.jsx
index e412f9c..eefa053 100644
--- a/__tests__/helpers/components/CollectionDynamicAdded.jsx
+++ b/__tests__/helpers/components/CollectionDynamicAdded.jsx
@@ -18,7 +18,7 @@ export default function CollectionDynamicAdded() {
onlyNumberCollection
]);
- const [collections, setCollection] = useChildren([]);
+ const [collections, setCollection] = useChildren();
const index = useRef(0);
const addCollection = () => {
index.current = index.current + 1;
diff --git a/__tests__/helpers/components/FormWithValidationAfterMounted.jsx b/__tests__/helpers/components/FormWithValidationAfterMounted.jsx
new file mode 100644
index 0000000..ec18b62
--- /dev/null
+++ b/__tests__/helpers/components/FormWithValidationAfterMounted.jsx
@@ -0,0 +1,29 @@
+import React, { useState } from "react";
+import Submit from "./Submit";
+import { Form, Input, useValidation } from "./../../../src";
+
+const required = value =>
+ value && value.trim() !== "" ? undefined : "Required";
+
+export const FormWithValidationAfterMounted = () => {
+ const [input, setInput] = useState(() => null);
+
+ const [status, inputValidation] = useValidation([required]);
+
+ const onClick = () => {
+ setInput();
+ };
+
+ return (
+ <>
+
+
+ {status.error && }
+ >
+ );
+};
diff --git a/__tests__/helpers/components/PersistStateOnUnmountHelpers.jsx b/__tests__/helpers/components/PersistStateOnUnmountHelpers.jsx
new file mode 100644
index 0000000..4b23114
--- /dev/null
+++ b/__tests__/helpers/components/PersistStateOnUnmountHelpers.jsx
@@ -0,0 +1,138 @@
+import React, { useState } from "react";
+import {
+ Form,
+ Collection,
+ Input,
+ Select,
+ PersistStateOnUnmount
+} from "./../../../src";
+import Submit from "./Submit";
+import Email from "./Email";
+import TextField from "./TextField";
+import Reset from "./Reset";
+
+export const initialState = {
+ keepValue: "keepValue",
+ user: {
+ name: "foo",
+ lastname: "micky",
+ email: "abc@test.it"
+ },
+ gender: "F",
+ other: ["1", "3"],
+ select: "1"
+};
+
+export const PersistStateOnUnmountHelpers = props => {
+ const [showCollection, setShowCollection] = useState(true);
+ const [showShowNestedInput, setShowNestedInput] = useState(true);
+ const [showShowRadio, setShowRadio] = useState(true);
+ const [showShowSelect, setShowShowSelect] = useState(true);
+
+ const [showkeepValue, setkeepValue] = useState(true);
+
+ const togglekeepValue = () => {
+ setkeepValue(prev => !prev);
+ };
+
+ const toggleSelect = () => {
+ setShowShowSelect(prev => !prev);
+ };
+
+ const toggleNestedRadio = () => {
+ setShowRadio(prev => !prev);
+ };
+
+ const toggleNestedInput = () => {
+ setShowNestedInput(prev => !prev);
+ };
+
+ const toggleCollection = () => {
+ setShowCollection(prev => !prev);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/__tests__/helpers/components/Radio.jsx b/__tests__/helpers/components/Radio.jsx
new file mode 100644
index 0000000..eacbca7
--- /dev/null
+++ b/__tests__/helpers/components/Radio.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { Input, useValidation } from "./../../../src";
+
+const required = value => (value ? undefined : "Required");
+
+export default function Radio({ name = "radio", value }) {
+ const [status, validation] = useValidation([required]);
+ return (
+
+
+
+ {status.error && }
+
+ );
+}
diff --git a/docs/PersistStateOnUnmount.mdx b/docs/PersistStateOnUnmount.mdx
new file mode 100644
index 0000000..40699d0
--- /dev/null
+++ b/docs/PersistStateOnUnmount.mdx
@@ -0,0 +1,42 @@
+---
+name: PersistStateOnUnmount
+menu: Components
+---
+
+import { Playground } from 'docz';
+import { useState } from "react";
+import { Form } from "./helpers/Form";
+import { InputLabel as Input } from "./helpers/InputLabel";
+import { PersistStateOnUnmount, Collection } from './../src';
+
+# PersistStateOnUnmount
+In `usetheform` if a Field gets unmounted its value within the Form state gets cleared.
+Wrap your Field elements between the `` component to preserve the Fields values.
+
+
+## Basic usage
+
+```javascript
+ import { Form, PersistStateOnUnmount, Input } from 'usetheform'
+```
+
+
+{() => {
+ const [visible, toggle] = useState(false);
+ return (
+
+ )}
+}
+
\ No newline at end of file
diff --git a/examples/helpers/SimpleForm.js b/examples/helpers/SimpleForm.js
index 6c11cce..92e84f0 100644
--- a/examples/helpers/SimpleForm.js
+++ b/examples/helpers/SimpleForm.js
@@ -36,7 +36,6 @@ const asyncTest = value =>
new Promise((resolve, reject) => {
// it could be an API call or any async operation
- console.log("asyncTest", value.mailList);
setTimeout(() => {
if (!value && !value.user && !value.user.mailList) {
reject("Empty are not allowed");
diff --git a/package-lock.json b/package-lock.json
index 6ac4d2a..4aa5131 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "usetheform",
- "version": "3.4.0",
+ "version": "3.4.1-alpha.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 3504a62..25d6bb3 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "usetheform",
- "version": "3.4.0",
+ "version": "3.4.1-alpha.4",
"description": "React library for composing declarative forms in React and managing their state.",
"main": "./build/index.js",
"module": "./build/es/index.js",
diff --git a/src/PersistStateOnUnmount.js b/src/PersistStateOnUnmount.js
new file mode 100644
index 0000000..13373e0
--- /dev/null
+++ b/src/PersistStateOnUnmount.js
@@ -0,0 +1,11 @@
+import React, { memo } from "react";
+import { ContextObject as Context, useOwnContext } from "./hooks/useOwnContext";
+
+const preventRemoval = () => false;
+export const PersistStateOnUnmount = memo(function PersistStateOnUnmount({
+ children
+}) {
+ const context = useOwnContext();
+ const newContext = { ...context, removeProp: preventRemoval };
+ return {children};
+});
diff --git a/src/hooks/useField.js b/src/hooks/useField.js
index dfd5384..df17987 100755
--- a/src/hooks/useField.js
+++ b/src/hooks/useField.js
@@ -104,6 +104,7 @@ export function useField(props) {
const reset = useCallback(formState => {
valueFieldLastAsyncCheck.current = null;
valueFieldLastSyncCheck.current = null;
+ onFirstBlur.current = false;
switch (type) {
case "number":
@@ -246,6 +247,8 @@ export function useField(props) {
const newValue = applyReducers(val, initialValue, formState.current);
context.initProp(nameProp.current, newValue, val);
+ } else if (validators.length > 0 && context.stillMounted()) {
+ context.runSyncValidation();
}
return () => {
@@ -271,6 +274,7 @@ export function useField(props) {
},
true
);
+
context.unRegisterReset(nameProp.current);
if (context.type === "array") {
context.removeIndex(uniqueIDarrayContext);
@@ -304,7 +308,8 @@ export function useField(props) {
context.formStatus !== STATUS.ON_INIT_ASYNC
) {
const firstTimeCheck = valueFieldLastSyncCheck.current === null;
- const onlyShowOnSubmit = type === "radio" || type === "checkbox";
+ const isRadioOrCheckBox = type === "radio" || type === "checkbox";
+ const onlyShowOnSubmit = isRadioOrCheckBox;
const isCustomCmp = type === "custom";
const forceOnBlur = type === "select" && multiple;
@@ -315,14 +320,18 @@ export function useField(props) {
(isCustomCmp && touched && onSyncBlurState) ||
context.formStatus === STATUS.ON_SUBMIT ||
(!onlyShowOnSubmit &&
- ((touched && onSyncBlurState) ||
+ ((touched && onFirstBlur.current) ||
(!touched &&
forceOnBlur &&
(onSyncBlurState || firstTimeCheck)) ||
(!touched && !forceOnBlur))) ||
(onlyShowOnSubmit && onSyncFocusState))
) {
- valueFieldLastSyncCheck.current = valueField.current;
+ if (isRadioOrCheckBox) {
+ valueFieldLastSyncCheck.current = checkedField.current;
+ } else {
+ valueFieldLastSyncCheck.current = valueField.current;
+ }
onValidation(
validationObj.current.checks,
@@ -368,8 +377,10 @@ export function useField(props) {
context.formStatus
]);
+ const onFirstBlur = useRef(false);
const onBlur = useCallback(e => {
e.persist();
+ onFirstBlur.current = true;
setAsyncOnBlur(true);
setSyncOnBlur(true);
customBlur(e);
diff --git a/src/hooks/useForm.js b/src/hooks/useForm.js
index bd61f90..333463f 100755
--- a/src/hooks/useForm.js
+++ b/src/hooks/useForm.js
@@ -327,6 +327,13 @@ export function useForm({
}
}, []);
+ const runSyncValidation = useCallback(() => {
+ const isValid = isFormValid(validators.current, stateRef.current.state);
+
+ stateRef.current.isValid !== isValid &&
+ dispatchFormState({ ...stateRef.current, isValid });
+ }, []);
+
const runAsyncValidation = useCallback(({ start, end }) => {
if (start) {
const status = STATUS.ON_RUN_ASYNC;
@@ -457,6 +464,7 @@ export function useForm({
updateRegisteredField,
registerAsyncInitValidation,
runAsyncValidation,
+ runSyncValidation,
dispatchNewState,
changeProp,
initProp,
diff --git a/src/hooks/useObject.js b/src/hooks/useObject.js
index 55e61b6..96c4df7 100755
--- a/src/hooks/useObject.js
+++ b/src/hooks/useObject.js
@@ -379,7 +379,6 @@ export function useObject(props) {
);
}
// ----- remove validators inerithed by children ----- //
-
context.removeProp(
nameProp.current,
{
@@ -422,6 +421,7 @@ export function useObject(props) {
formState: context.formState, // pass the global form state down
formStatus: context.formStatus, // pass the global form status down
runAsyncValidation: context.runAsyncValidation,
+ runSyncValidation: context.runSyncValidation,
unRegisterField,
updateRegisteredField,
registerAsyncInitValidation,
diff --git a/src/index.d.ts b/src/index.d.ts
index 2bc0f4f..40fbc53 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -5,6 +5,7 @@ declare module "usetheform" {
export const Select: any;
export const TextArea: any;
export const Collection: any;
+ export const PersistStateOnUnmount: any;
export const FormContext: any;
export const getValueByPath: any;
export const STATUS: any;
diff --git a/src/index.js b/src/index.js
index 3fb0a88..ae09204 100755
--- a/src/index.js
+++ b/src/index.js
@@ -4,6 +4,7 @@ export { Input } from "./Input";
export { Select } from "./Select";
export { TextArea } from "./TextArea";
export { Collection } from "./Collection";
+export { PersistStateOnUnmount } from "./PersistStateOnUnmount";
export { FormContext } from "./FormContext";
export { getValueByPath } from "./utils/formUtils";
export { STATUS } from "./utils/constants";