Skip to content

Commit

Permalink
Enabling sync and async validation at form level (#29)
Browse files Browse the repository at this point in the history
Co-authored-by: Antonio <[email protected]>
  • Loading branch information
antoniopangallo and Antonio authored Jan 26, 2021
1 parent fc5179c commit c22fda5
Show file tree
Hide file tree
Showing 52 changed files with 4,261 additions and 2,065 deletions.
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@
"browser": true,
"node": true,
"jest": true
},
"settings": {
"react": {
"version": "latest"
}
}
}
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ deploy:
github_token: $GITHUB_TOKEN
on:
branch: master
after_success:
- npm run coveralls
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<h3 align="center">An easy way for building forms in React.</h3><br/>

<p align="center">
<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>
<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>
<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>
<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>
</p><br/><br/>


<div align="center">
<p align="center">
<a href="https://iusehooks.github.io/usetheform/" title="Usetheform">
Expand All @@ -19,25 +19,33 @@

## :bulb: What is usetheform about?

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.
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.

- [Documentation](https://iusehooks.github.io/usetheform/)
- [Installation](#Installation)
- [Features](#fire-features)
- [Quickstart](#zap-quickstart)
- [Motivation](#motivation)
- [Code Sandboxes Examples](#code-sandboxes)
- [Contributing](#contributing)
- [License](#license)

✅ Zero dependencies
## :fire: Features

✅ Only peer dependencies: React >= 16.8.0
- 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).
- 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).
- 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).
- Follows HTML standard for validation. 👉🏻 [Play with HTML built-in form validation](https://codesandbox.io/s/built-informvalidation-lp672?file=/src/Info.jsx).
- 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).
- Easy to handle arrays, objects or nested collections. 👉🏻 [Play with nested collections](https://iusehooks.github.io/usetheform/docs-collection#nested-collections).
- Tiny size with zero dependencies. 👉🏻 [Check size](https://bundlephobia.com/result?p=usetheform).
- Typescript supported.

## Installation
## :zap: Quickstart

```sh
npm install --save usetheform
```

## :zap: Quickstart

```jsx
import React from "react";
import Form, { Input, useValidation } from "usetheform";
Expand All @@ -64,6 +72,12 @@ export default function App() {
}
```

## Motivation

**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.

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 🙏🏻.

## Author

- Antonio Pangallo [@antonio_pangall](https://twitter.com/antonio_pangall)
Expand All @@ -73,10 +87,18 @@ export default function App() {
- Twitter What's Happening Form Bar: [Sandbox](https://codesandbox.io/s/twitter-bar-form-czx3o)
- Shopping Cart: [Sandbox](https://codesandbox.io/s/shopping-cart-97y5k)
- Examples: Slider, Select, Collections etc..: [Sandbox](https://codesandbox.io/s/formexample2-mmcjs)
- Various Implementation: [Sandbox](https://codesandbox.io/s/035l4l75ln)
- Validation using Yup, ZOD, JOI, Superstruct: [Sandbox](https://codesandbox.io/s/schema-validations-uc1m6)
- Wizard: [Sandbox](https://codesandbox.io/s/v680xok7k7)
- FormContext: [Sandbox](https://codesandbox.io/s/formcontext-ukvc5)
- Material UI - React Select: [Sandbox](https://codesandbox.io/s/materialuireactselect-6ufc2)
- Material UI - React Select: [Sandbox](https://codesandbox.io/s/materialuireactselect-6ufc2)
- React Dropzone - Material UI Dropzone: [Sandbox](https://codesandbox.io/s/reactdropzone-materialuidropzone-yjb8w)
- Various Implementation: [Sandbox](https://codesandbox.io/s/035l4l75ln)

## Contributing

🎉 First off, thanks for taking the time to contribute! 🎉

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).

## License

Expand Down
58 changes: 57 additions & 1 deletion __tests__/Collection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { fireEvent, waitFor, cleanup, act } from "@testing-library/react";

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

import { CollectionDynamicCart } from "./helpers/components/CollectionDynamicField";
import {
CollectionDynamicCart,
CollectionObjectDynamicField
} from "./helpers/components/CollectionDynamicField";
import CollectionDynamicAdded from "./helpers/components/CollectionDynamicAdded";
import CollectionValidation, {
CollectionValidationTouched
Expand Down Expand Up @@ -582,6 +585,27 @@ describe("Component => Collection", () => {
true
);
});
it("should add/remove fields dyncamically from a object Collection", () => {
const props = { onInit, onChange };
const children = [<CollectionObjectDynamicField key="1" />];

const { getByTestId } = mountForm({ children, props });

const addInput = getByTestId("addInput");
const removeInput = getByTestId("removeInput");
expect(onInit).toHaveBeenCalledWith({}, true);
act(() => {
fireEvent.click(addInput);
});

expect(onChange).toHaveBeenCalledWith({ dynamic: { 1: "1" } }, true);

act(() => {
fireEvent.click(removeInput);
});

expect(onChange).toHaveBeenCalledWith({}, true);
});

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

console.error = originalError;
});

it("should throw an error if the a prop 'name' is used within an array Collection", () => {
const originalError = console.error;
console.error = jest.fn();
let children = [
<Collection key="1" array name="array">
<Input type="text" name="abc" />
</Collection>
];
expect(() => mountForm({ children })).toThrowError(
/it is not allowed within context a of type \"array\"/i
);

console.error = originalError;
});

it("should throw an error for an invalid 'asyncValidator' prop", () => {
const originalError = console.error;
console.error = jest.fn();
let children = [
<Collection key="1" array name="array" asyncValidator={true}>
<Collection object>
<Input type="text" name="test" />
</Collection>
</Collection>
];
expect(() => mountForm({ children })).toThrowError(
/It must be a function/i
);

console.error = originalError;
});
});
128 changes: 128 additions & 0 deletions __tests__/CollectionArrayIndexHandledManually.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from "react";
import { fireEvent, cleanup, act } from "@testing-library/react";

import { CollectionArrayIndexHandledManually } from "./helpers/components/CollectionArrayIndexHandledManually";

import Reset from "./helpers/components/Reset";
import Submit from "./helpers/components/Submit";
import { mountForm } from "./helpers/utils/mountForm";

const onInit = jest.fn();
const onChange = jest.fn();
const onReset = jest.fn();
const onSubmit = jest.fn();

afterEach(cleanup);

describe("Component => Collection (Array with indexes handled manually)", () => {
beforeEach(() => {
onInit.mockClear();
onChange.mockClear();
onReset.mockClear();
onSubmit.mockClear();
});

it("should correctly render an array Collection with indexes handled manually", () => {
const props = { onInit, onChange, onReset, onSubmit };
const myself = { current: null };

const children = [
<CollectionArrayIndexHandledManually key="1" ref={myself} />,
<Reset key="2" />,
<Submit key="3" />
];
const { getByTestId } = mountForm({ props, children });
const addInput = getByTestId("addInput");
const addCollection = getByTestId("addCollection");
const removeCollection = getByTestId("removeCollection");

const removeInput = getByTestId("removeInput");
const reset = getByTestId("reset");
const submit = getByTestId("submit");

expect(onInit).toHaveBeenCalledWith({}, true);

for (let i = 1; i <= 10; i++) {
act(() => {
fireEvent.click(addInput);
});
}

let stateExpected = myself.current.getInnerState();
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);

for (let i = 1; i <= 5; i++) {
act(() => {
fireEvent.click(removeInput);
});
}

stateExpected = myself.current.getInnerState();
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);

const newExpected = [];
stateExpected[0].forEach(val => {
const input = getByTestId(`input_${val}`);
const newValue = Math.random() * 10000;
newExpected.push(`${newValue}`);
act(() => {
fireEvent.change(input, { target: { value: `${newValue}` } });
});
});

expect(onChange).toHaveBeenCalledWith({ indexManual: [newExpected] }, true);

act(() => {
fireEvent.click(submit);
});

expect(onSubmit).toHaveBeenCalledWith({ indexManual: [newExpected] }, true);

act(() => {
fireEvent.click(reset);
});

expect(onReset).toHaveBeenCalledWith({ indexManual: stateExpected }, true);

for (let i = 1; i <= 10; i++) {
act(() => {
fireEvent.click(addCollection);
});
}

stateExpected = myself.current.getInnerState();
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);

const newCollectionExpected = [];
stateExpected[1].forEach(val => {
const input = getByTestId(`text_${val[0]}`);
const newValue = Math.random() * 10000;
newCollectionExpected.push([`${newValue}`]);
act(() => {
fireEvent.change(input, { target: { value: `${newValue}` } });
});
});

const nextStateExpected = [stateExpected[0], newCollectionExpected];
expect(onChange).toHaveBeenCalledWith(
{ indexManual: nextStateExpected },
true
);

stateExpected = myself.current.getInnerState();
act(() => {
fireEvent.click(reset);
});

expect(onReset).toHaveBeenCalledWith({ indexManual: stateExpected }, true);

for (let i = 1; i <= 5; i++) {
act(() => {
fireEvent.click(removeCollection);
});
}

stateExpected = myself.current.getInnerState();
expect(onChange).toHaveBeenCalledWith({ indexManual: stateExpected }, true);
});
});
Loading

0 comments on commit c22fda5

Please sign in to comment.