Skip to content

Commit c22fda5

Browse files
antoniopangalloAntonio
andauthored
Enabling sync and async validation at form level (#29)
Co-authored-by: Antonio <[email protected]>
1 parent fc5179c commit c22fda5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4261
-2065
lines changed

.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,10 @@
3333
"browser": true,
3434
"node": true,
3535
"jest": true
36+
},
37+
"settings": {
38+
"react": {
39+
"version": "latest"
40+
}
3641
}
3742
}

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ deploy:
1717
github_token: $GITHUB_TOKEN
1818
on:
1919
branch: master
20+
after_success:
21+
- npm run coveralls

README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
<h3 align="center">An easy way for building forms in React.</h3><br/>
44

55
<p align="center">
6+
<a href="https://coveralls.io/github/iusehooks/usetheform?branch=master"><img src="https://coveralls.io/repos/github/iusehooks/usetheform/badge.svg?branch=master" alt="Code Coverage" height="20"/></a>
67
<a href="https://travis-ci.org/iusehooks/usetheform"><img src="https://travis-ci.org/iusehooks/usetheform.svg?branch=master" alt="Build info" height="20"/></a>
78
<a href="https://bundlephobia.com/result?p=usetheform@latest"><img src="https://img.shields.io/bundlephobia/minzip/usetheform.svg" alt="Bundle size" height="20"/></a>
89
<a href="https://twitter.com/intent/tweet?text=React%20library%20for%20composing%20declarative%20forms%2C%20manage%20their%20state%2C%20handling%20their%20validation%20and%20much%20more&url=https://github.com/iusehooks/usetheform&hashtags=reactjs,webdev,javascript,forms,reacthooks"><img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social" alt="Tweet" height="20"/></a>
910
</p><br/><br/>
1011

11-
1212
<div align="center">
1313
<p align="center">
1414
<a href="https://iusehooks.github.io/usetheform/" title="Usetheform">
@@ -19,25 +19,33 @@
1919

2020
## :bulb: What is usetheform about?
2121

22-
Usetheform is a React library for composing declarative forms and managing their state. It uses the Context API and React Hooks. I does not depend on any library like redux or others.
22+
Welcome! 👋 Usetheform is a React library for composing declarative forms and managing their state. It does not depend on any external library like Redux, MobX or others, which makes it to be easily adoptedable without other dependencies.
2323

2424
- [Documentation](https://iusehooks.github.io/usetheform/)
25-
- [Installation](#Installation)
25+
- [Features](#fire-features)
26+
- [Quickstart](#zap-quickstart)
27+
- [Motivation](#motivation)
2628
- [Code Sandboxes Examples](#code-sandboxes)
29+
- [Contributing](#contributing)
2730
- [License](#license)
2831

29-
✅ Zero dependencies
32+
## :fire: Features
3033

31-
✅ Only peer dependencies: React >= 16.8.0
34+
- Easy integration with other libraries. 👉🏻 [Play with React Select/Material UI](https://codesandbox.io/s/materialuireactselect-6ufc2) - [React Dropzone/MaterialUI Dropzone](https://codesandbox.io/s/reactdropzone-materialuidropzone-yjb8w).
35+
- Support Sync and Async validation at [Form](https://iusehooks.github.io/usetheform/docs-form#validation---sync), [Field](https://iusehooks.github.io/usetheform/docs-input#validation---sync) and [Collection](https://iusehooks.github.io/usetheform/docs-collection#validation---sync) level. 👉🏻 [Play with Sync and Async validation](https://iusehooks.github.io/usetheform/docs-input#validation---sync).
36+
- Support [Yup](https://codesandbox.io/s/schema-validations-uc1m6?file=/src/FormYUP.jsx), [Zod](https://codesandbox.io/s/schema-validations-uc1m6?file=/src/FormZOD.jsx), [Superstruct](https://codesandbox.io/s/schema-validations-uc1m6?file=/src/FormSuperStruct.jsx), [Joi](https://codesandbox.io/s/schema-validations-uc1m6?file=/src/FormJOI.jsx) or custom. 👉🏻 [Play with YUP - ZOD - Superstruct - Joi validations](https://codesandbox.io/s/schema-validations-uc1m6).
37+
- Follows HTML standard for validation. 👉🏻 [Play with HTML built-in form validation](https://codesandbox.io/s/built-informvalidation-lp672?file=/src/Info.jsx).
38+
- Support reducers functions at [Form](https://iusehooks.github.io/usetheform/docs-form#reducers), [Field](https://iusehooks.github.io/usetheform/docs-input#reducers) and [Collection](https://iusehooks.github.io/usetheform/docs-collection#reducers) level. 👉🏻 [Play with Reducers](https://iusehooks.github.io/usetheform/docs-form#reducers).
39+
- Easy to handle arrays, objects or nested collections. 👉🏻 [Play with nested collections](https://iusehooks.github.io/usetheform/docs-collection#nested-collections).
40+
- Tiny size with zero dependencies. 👉🏻 [Check size](https://bundlephobia.com/result?p=usetheform).
41+
- Typescript supported.
3242

33-
## Installation
43+
## :zap: Quickstart
3444

3545
```sh
3646
npm install --save usetheform
3747
```
3848

39-
## :zap: Quickstart
40-
4149
```jsx
4250
import React from "react";
4351
import Form, { Input, useValidation } from "usetheform";
@@ -64,6 +72,12 @@ export default function App() {
6472
}
6573
```
6674

75+
## Motivation
76+
77+
**usetheform** has been built having in mind the necessity of developing a lightweight library able to provide an easy API to build complex forms composed by nested levels (arrays, objects, custom inputs, etc.) with a declarative approach and without the need to include external libraries within your react projects.
78+
79+
It's easy to start using it in your existing project and gives you a full controll over Field, Collection at any level of nesting which makes easy to manipulate the form state based on your needs. Synchronous and asynchronous validations are simple and error messages easy to customize and display. If you find it useful please leave a star 🙏🏻.
80+
6781
## Author
6882

6983
- Antonio Pangallo [@antonio_pangall](https://twitter.com/antonio_pangall)
@@ -73,10 +87,18 @@ export default function App() {
7387
- Twitter What's Happening Form Bar: [Sandbox](https://codesandbox.io/s/twitter-bar-form-czx3o)
7488
- Shopping Cart: [Sandbox](https://codesandbox.io/s/shopping-cart-97y5k)
7589
- Examples: Slider, Select, Collections etc..: [Sandbox](https://codesandbox.io/s/formexample2-mmcjs)
76-
- Various Implementation: [Sandbox](https://codesandbox.io/s/035l4l75ln)
90+
- Validation using Yup, ZOD, JOI, Superstruct: [Sandbox](https://codesandbox.io/s/schema-validations-uc1m6)
7791
- Wizard: [Sandbox](https://codesandbox.io/s/v680xok7k7)
7892
- FormContext: [Sandbox](https://codesandbox.io/s/formcontext-ukvc5)
79-
- Material UI - React Select: [Sandbox](https://codesandbox.io/s/materialuireactselect-6ufc2)
93+
- Material UI - React Select: [Sandbox](https://codesandbox.io/s/materialuireactselect-6ufc2)
94+
- React Dropzone - Material UI Dropzone: [Sandbox](https://codesandbox.io/s/reactdropzone-materialuidropzone-yjb8w)
95+
- Various Implementation: [Sandbox](https://codesandbox.io/s/035l4l75ln)
96+
97+
## Contributing
98+
99+
🎉 First off, thanks for taking the time to contribute! 🎉
100+
101+
We would like to encourage everyone to help and support this library by contributing. See the [CONTRIBUTING file](https://github.com/iusehooks/usetheform/blob/master/CONTRIBUTING.md).
80102

81103
## License
82104

__tests__/Collection.spec.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { fireEvent, waitFor, cleanup, act } from "@testing-library/react";
33

44
import { Input, Collection } from "./../src";
55

6-
import { CollectionDynamicCart } from "./helpers/components/CollectionDynamicField";
6+
import {
7+
CollectionDynamicCart,
8+
CollectionObjectDynamicField
9+
} from "./helpers/components/CollectionDynamicField";
710
import CollectionDynamicAdded from "./helpers/components/CollectionDynamicAdded";
811
import CollectionValidation, {
912
CollectionValidationTouched
@@ -582,6 +585,27 @@ describe("Component => Collection", () => {
582585
true
583586
);
584587
});
588+
it("should add/remove fields dyncamically from a object Collection", () => {
589+
const props = { onInit, onChange };
590+
const children = [<CollectionObjectDynamicField key="1" />];
591+
592+
const { getByTestId } = mountForm({ children, props });
593+
594+
const addInput = getByTestId("addInput");
595+
const removeInput = getByTestId("removeInput");
596+
expect(onInit).toHaveBeenCalledWith({}, true);
597+
act(() => {
598+
fireEvent.click(addInput);
599+
});
600+
601+
expect(onChange).toHaveBeenCalledWith({ dynamic: { 1: "1" } }, true);
602+
603+
act(() => {
604+
fireEvent.click(removeInput);
605+
});
606+
607+
expect(onChange).toHaveBeenCalledWith({}, true);
608+
});
585609

586610
it("should run reducer functions on Collection fields removal", () => {
587611
const props = { onSubmit, onChange, onReset };
@@ -653,4 +677,36 @@ describe("Component => Collection", () => {
653677

654678
console.error = originalError;
655679
});
680+
681+
it("should throw an error if the a prop 'name' is used within an array Collection", () => {
682+
const originalError = console.error;
683+
console.error = jest.fn();
684+
let children = [
685+
<Collection key="1" array name="array">
686+
<Input type="text" name="abc" />
687+
</Collection>
688+
];
689+
expect(() => mountForm({ children })).toThrowError(
690+
/it is not allowed within context a of type \"array\"/i
691+
);
692+
693+
console.error = originalError;
694+
});
695+
696+
it("should throw an error for an invalid 'asyncValidator' prop", () => {
697+
const originalError = console.error;
698+
console.error = jest.fn();
699+
let children = [
700+
<Collection key="1" array name="array" asyncValidator={true}>
701+
<Collection object>
702+
<Input type="text" name="test" />
703+
</Collection>
704+
</Collection>
705+
];
706+
expect(() => mountForm({ children })).toThrowError(
707+
/It must be a function/i
708+
);
709+
710+
console.error = originalError;
711+
});
656712
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React from "react";
2+
import { fireEvent, cleanup, act } from "@testing-library/react";
3+
4+
import { CollectionArrayIndexHandledManually } from "./helpers/components/CollectionArrayIndexHandledManually";
5+
6+
import Reset from "./helpers/components/Reset";
7+
import Submit from "./helpers/components/Submit";
8+
import { mountForm } from "./helpers/utils/mountForm";
9+
10+
const onInit = jest.fn();
11+
const onChange = jest.fn();
12+
const onReset = jest.fn();
13+
const onSubmit = jest.fn();
14+
15+
afterEach(cleanup);
16+
17+
describe("Component => Collection (Array with indexes handled manually)", () => {
18+
beforeEach(() => {
19+
onInit.mockClear();
20+
onChange.mockClear();
21+
onReset.mockClear();
22+
onSubmit.mockClear();
23+
});
24+
25+
it("should correctly render an array Collection with indexes handled manually", () => {
26+
const props = { onInit, onChange, onReset, onSubmit };
27+
const myself = { current: null };
28+
29+
const children = [
30+
<CollectionArrayIndexHandledManually key="1" ref={myself} />,
31+
<Reset key="2" />,
32+
<Submit key="3" />
33+
];
34+
const { getByTestId } = mountForm({ props, children });
35+
const addInput = getByTestId("addInput");
36+
const addCollection = getByTestId("addCollection");
37+
const removeCollection = getByTestId("removeCollection");
38+
39+
const removeInput = getByTestId("removeInput");
40+
const reset = getByTestId("reset");
41+
const submit = getByTestId("submit");
42+
43+
expect(onInit).toHaveBeenCalledWith({}, true);
44+
45+
for (let i = 1; i <= 10; i++) {
46+
act(() => {
47+
fireEvent.click(addInput);
48+
});
49+
}
50+
51+
let stateExpected = myself.current.getInnerState();
52+
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
53+
54+
for (let i = 1; i <= 5; i++) {
55+
act(() => {
56+
fireEvent.click(removeInput);
57+
});
58+
}
59+
60+
stateExpected = myself.current.getInnerState();
61+
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
62+
63+
const newExpected = [];
64+
stateExpected[0].forEach(val => {
65+
const input = getByTestId(`input_${val}`);
66+
const newValue = Math.random() * 10000;
67+
newExpected.push(`${newValue}`);
68+
act(() => {
69+
fireEvent.change(input, { target: { value: `${newValue}` } });
70+
});
71+
});
72+
73+
expect(onChange).toHaveBeenCalledWith({ indexManual: [newExpected] }, true);
74+
75+
act(() => {
76+
fireEvent.click(submit);
77+
});
78+
79+
expect(onSubmit).toHaveBeenCalledWith({ indexManual: [newExpected] }, true);
80+
81+
act(() => {
82+
fireEvent.click(reset);
83+
});
84+
85+
expect(onReset).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
86+
87+
for (let i = 1; i <= 10; i++) {
88+
act(() => {
89+
fireEvent.click(addCollection);
90+
});
91+
}
92+
93+
stateExpected = myself.current.getInnerState();
94+
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
95+
96+
const newCollectionExpected = [];
97+
stateExpected[1].forEach(val => {
98+
const input = getByTestId(`text_${val[0]}`);
99+
const newValue = Math.random() * 10000;
100+
newCollectionExpected.push([`${newValue}`]);
101+
act(() => {
102+
fireEvent.change(input, { target: { value: `${newValue}` } });
103+
});
104+
});
105+
106+
const nextStateExpected = [stateExpected[0], newCollectionExpected];
107+
expect(onChange).toHaveBeenCalledWith(
108+
{ indexManual: nextStateExpected },
109+
true
110+
);
111+
112+
stateExpected = myself.current.getInnerState();
113+
act(() => {
114+
fireEvent.click(reset);
115+
});
116+
117+
expect(onReset).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
118+
119+
for (let i = 1; i <= 5; i++) {
120+
act(() => {
121+
fireEvent.click(removeCollection);
122+
});
123+
}
124+
125+
stateExpected = myself.current.getInnerState();
126+
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
127+
});
128+
});

0 commit comments

Comments
 (0)