diff --git a/README.md b/README.md
index 605172b3..ce8e2dc5 100644
--- a/README.md
+++ b/README.md
@@ -14,103 +14,61 @@
### 🏠 [Homepage](https://useform.org)
### ✨ [Demo](https://codesandbox.io/s/useform-2u2ju)
+# UseForm
-
-
-
+> Create hooks to manage your forms.
-## Motivation
-
-There are many ways to create forms in React, and there are many form libraries available, with different features. I must admit that there a lot of good form libraries,
-so, why one more?
-
-UseForm was born because I found great difficulty when I wanted to build complex forms with different steps or levels and with many fields.
-When we need to build complex forms we can encounter issues like:
-
-- A lot of rendering - Every change is made in the form state is reflected and the form component tree is rendered again and again.
-- A lot of properties - When you use some libraries it is necessary to put many properties in one input, creating a lot of unnecessary code.
-- Just one approach - You can use controlled forms or uncontrolled forms never both together in the same library.
-
-UseForm is a library that solves all these problems.
-## Description
+UseForm is an open source project that allow you to create form easily, different from the others options, this package guide you to create custom hooks to manage your forms, you can use the same form in different components without context API.
-Forms are an important part of web applications, and with react it's possible to create greats forms,
-react hooks are a game-changer when we think about forms, with hooks it is very simple to create forms, and you can go on without libraries.
-But when we wanna complex forms with many validations and nested objects with several layer and properties is appropriate to use a library form to manager the state of inputs and its validations.
-For this reason, there is useForm, with useForm we can make greats forms and complex validations with less line code.
+ - As other packages, you can also use yup validation to validate your form.
+ - You can also use different approach to handle your form, like `onSubmit | onChange | debounce`.
+ - Less code, than other options.
-```jsx
-// FORMIk EXAMPLE
-
-
-//USEFORM EXAMPLE
-
-```
-
-UseForm provides a way to create complex forms easily, this hook returns an object of values in the same shape that it receives, this is possible using dot notation. Therefore,
-it does not matter if there is a nested object or has many properties or array,
-the result is the same. This process turns very easily to create forms from nested objects,
-the same layers and properties are replicated in the final object,
-this approach prevents you to type more code to convert an object from form to backend object type. The same process is realized with errors object and touched objects.
-
-## What to expect with useForm
+## Motivation
-- Performer forms - useForm provides a way to complete a form and submit it without any rerender, by default useForm creates uncontrolled forms.
-- Easy to write - useForm has an easy way to write forms with less code.
- register function return necessary input's properties and it is all we need to manage all events in a native HTML `input`. Also, you can write forms without form tag.
-- Easy validation - By default useform uses yup validation, we can write complex validation without effort.
-- Easy to use - useForm is very easy to use, you can register a field with a single line of code.
-- No dependencies - useForm does not depend on any library to work.
+Today we have a lot of form packages, and this project don't pretend to be the number one, this is just a new way to create hooks to manage your forms. But if you guys like this project, we can publish it, and maintain it.
-## Installation
+## First step
+The first step is to create your form with the `createForm` function, this function returns a hook that you can use to manage your form, wherever you want to use.
-```
- npm i @use-form/use-form
-```
+``` javascript
+export const useLoginForm = createForm({
+ initialValues: {
+ email: 'juciano@juciano.com',
+ password: 'yourpassword',
+ }
+})
```
- yarn add @use-form/use-form
-```
-
-## Usage
-
-`useForm` provides a `register` function, this function return all necessary properties to manage the input's events and validation.
-
-
+## Second step
+The second step is to create a component to render your form, you can use the `useLoginForm` hook to get the form state and manage it.
-```javascript
-import { useForm } from '@use-form/use-form'
-
-/*
- * initial Values optional
- */
-const initialValues = {
- name: 'Jesse',
- email: 'jesse@jesse.com',
- score: 25
-}
-
-const {
- register,
- state: { values }
-} = useForm({ initialValues, mode: 'onChange' })
+```jsx
+ import { useLoginForm } from 'react-create-form'
+
+ const LoginForm = () => {
+ const { handleSubmit, register } = useLoginForm()
+
+ function onSubmit(values) {
+ console.log(values)
+ }
+
+ return (
+
+ )
+ }
```
-Use dot notation to create nested objects or to map object values. Type an entry name and type or an entry property object.
-```jsx
-
-
-
-```
+# It's All.
+## Read the full documentation [here](https://useform.org/docs/).
### [Post](https://dev.to/jucian0/building-forms-with-useform-1cna)
## 🤝 Contributing
diff --git a/__tests__/debounce.test.ts b/__tests__/debounce.test.ts
deleted file mode 100644
index fd9c01b3..00000000
--- a/__tests__/debounce.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { debounce } from '../src/utils'
-
-describe('Test debounce function', () => {
- jest.useFakeTimers()
-
- it('should call immediately function callback', () => {
- const callback = jest.fn()
-
- const debounced = debounce(callback, 100, true)
-
- debounced()
- jest.advanceTimersByTime(0)
-
- expect(callback).toBeCalled()
- })
-
- it('should call function callback when the time is up', () => {
- const callback = jest.fn()
-
- const debounced = debounce(callback, 300)
-
- debounced()
- jest.advanceTimersByTime(300)
-
- expect(callback).toBeCalled()
- })
-})
diff --git a/__tests__/useForm.test.ts b/__tests__/useForm.test.ts
deleted file mode 100644
index bbf6b459..00000000
--- a/__tests__/useForm.test.ts
+++ /dev/null
@@ -1,639 +0,0 @@
-import {
- makeHandleChangeSut,
- makeRadioSut,
- makeSelectSut,
- makeSut,
- makeUseFormParamsMock
-} from './utils/makeSut'
-import * as faker from 'faker'
-import { waitFor } from '@testing-library/dom'
-import '@testing-library/jest-dom/extend-expect'
-import { act, fireEvent } from '@testing-library/react'
-
-describe('Test initial state', () => {
- test('Should set initial values', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- await waitFor(() => {
- expect(hookState.state.values).toEqual(mock.hookParams.initialValues)
- })
- })
-
- test('Should set initial errors', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- await waitFor(() => {
- expect(hookState.state.errors).toEqual(mock.hookParams.initialErrors)
- })
- })
-
- test('Should set initial touched', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- touched: faker.datatype.boolean()
- })
- const { hookState } = makeSut(mock)
- await waitFor(() => {
- expect(hookState.state.touched).toEqual(mock.hookParams.initialTouched)
- })
- })
-})
-
-describe('Test handle functions to setField/value/error/touched and resetField/value/error/touched', () => {
- test('Should change input state when run setFieldValue', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- const newValue = faker.random.word()
-
- act(() => {
- hookState.setFieldValue(mock.inputParams.name, newValue)
- })
-
- await waitFor(() => {
- expect(hookState.state.values.inputName).toEqual(newValue)
- })
- })
-
- test('Should change input state when run setFieldError', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- const newError = faker.random.words()
-
- act(() => {
- hookState.setFieldError(mock.inputParams.name, newError)
- })
-
- await waitFor(() => {
- expect(hookState.state.errors.inputName).toEqual(newError)
- })
- })
-
- test('Should change input state when run setFieldTouched', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- touched: false
- })
- const { hookState } = makeSut(mock)
- const newTouched = true
-
- act(() => {
- hookState.setFieldTouched(mock.inputParams.name, newTouched)
- })
-
- await waitFor(() => {
- expect(hookState.state.touched.inputName).toEqual(newTouched)
- })
- })
-
- test('Should reset input state when run resetFieldError', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- const newError = faker.random.words()
-
- act(() => {
- hookState.setFieldError(mock.inputParams.name, newError)
- })
-
- await waitFor(() => {
- expect(hookState.state.errors.inputName).toEqual(newError)
- })
-
- act(() => {
- hookState.resetFieldError(mock.inputParams.name)
- })
-
- await waitFor(() => {
- expect(hookState.state.errors.inputName).toEqual(
- mock.hookParams.initialErrors.inputName
- )
- })
- })
-
- test('Should reset input state when run resetFieldTouched', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- touched: false
- })
- const { hookState } = makeSut(mock)
- const newTouched = true
-
- act(() => {
- hookState.setFieldTouched(mock.inputParams.name, newTouched)
- })
-
- await waitFor(() => {
- expect(hookState.state.touched.inputName).toEqual(newTouched)
- })
-
- act(() => {
- hookState.resetFieldTouched(mock.inputParams.name)
- })
-
- await waitFor(() => {
- expect(hookState.state.touched.inputName).toEqual(
- mock.hookParams.initialTouched.inputName
- )
- })
- })
-
- test('Should reset input state when run resetFieldValue', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- const newValue = faker.random.word()
-
- act(() => {
- hookState.setFieldValue(mock.inputParams.name, newValue)
- })
-
- await waitFor(() => {
- expect(hookState.state.values.inputName).toEqual(newValue)
- })
-
- act(() => {
- hookState.resetFieldValue(mock.inputParams.name)
- })
-
- await waitFor(() => {
- expect(hookState.state.values.inputName).toEqual(
- mock.hookParams.initialValues.inputName
- )
- })
- })
-})
-
-describe('Test handle functions to setFields/value/error/touched and resetFields/values/errors/touched', () => {
- test('Should change input state when run setFieldsValue', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- const newValues = {
- inputName: faker.random.word(),
- inputEmail: faker.random.word()
- }
-
- act(() => {
- hookState.setFieldsValue(newValues)
- })
-
- await waitFor(() => {
- expect(hookState.state.values).toEqual(newValues)
- })
- })
-
- test('Should change input state when run setFieldsError', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- const newErrors = {
- inputName: faker.random.words(),
- inputEmail: faker.random.words()
- }
-
- act(() => {
- hookState.setFieldsError(newErrors)
- })
-
- await waitFor(() => {
- expect(hookState.state.errors).toEqual(newErrors)
- })
- })
-
- test('Should change input state when run setFieldsTouched', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- touched: faker.datatype.boolean()
- })
- const { hookState } = makeSut(mock)
- const newTouched = {
- inputName: faker.datatype.boolean(),
- inputEmail: faker.datatype.boolean()
- }
-
- act(() => {
- hookState.setFieldsTouched(newTouched)
- })
- })
-
- test('Should reset input state when run resetFieldsValue', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- const newValues = {
- inputName: faker.random.word(),
- inputEmail: faker.random.word()
- }
-
- act(() => {
- hookState.setFieldsValue(newValues)
- })
-
- await waitFor(() => {
- expect(hookState.state.values).toEqual(newValues)
- })
-
- act(() => {
- hookState.resetFieldsValue()
- })
-
- await waitFor(() => {
- expect(hookState.state.values).toEqual(mock.hookParams.initialValues)
- })
- })
-
- test('Should reset input state when run resetFieldsError', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- const newErrors = {
- inputName: faker.random.words(),
- inputEmail: faker.random.words()
- }
-
- act(() => {
- hookState.setFieldsError(newErrors)
- })
-
- await waitFor(() => {
- expect(hookState.state.errors).toEqual(newErrors)
- })
-
- act(() => {
- hookState.resetFieldsError()
- })
-
- await waitFor(() => {
- expect(hookState.state.errors).toEqual(mock.hookParams.initialErrors)
- })
- })
-
- test('Should reset input state when run resetFieldsTouched', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- touched: faker.datatype.boolean()
- })
- const { hookState } = makeSut(mock)
- const newTouched = {
- inputName: faker.datatype.boolean(),
- inputEmail: faker.datatype.boolean()
- }
-
- act(() => {
- hookState.setFieldsTouched(newTouched)
- })
-
- await waitFor(() => {
- expect(hookState.state.touched).toEqual(newTouched)
- })
-
- act(() => {
- hookState.resetFieldsTouched()
- })
-
- await waitFor(() => {
- expect(hookState.state.touched).toEqual(mock.hookParams.initialTouched)
- })
- })
-})
-
-describe('Test form handle functions', () => {
- test('Should change form state when run setForm', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word(),
- error: faker.random.words()
- })
- const { hookState } = makeSut(mock)
- const newState = {
- values: {
- inputName: faker.random.word()
- },
- errors: {},
- touched: {}
- }
-
- act(() => {
- hookState.setForm(newState)
- })
-
- await waitFor(() => {
- expect(hookState.state).toEqual(newState)
- })
- })
-
- test('Should change form state when run resetForm', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState } = makeSut(mock)
- const newState = {
- values: {
- inputName: faker.random.word()
- },
- errors: {},
- touched: {}
- }
-
- act(() => {
- hookState.setForm(newState)
- })
-
- await waitFor(() => {
- expect(hookState.state).toEqual(newState)
- })
-
- act(() => {
- hookState.resetForm()
- })
-
- await waitFor(() => {
- expect(hookState.state).toEqual({
- values: {
- inputName: mock.hookParams.initialValues.inputName
- },
- errors: { inputName: '' },
- touched: { inputName: false }
- })
- })
- })
-})
-
-describe('Tests input event and state manipulation', () => {
- test('Should update input value when dispatch input event', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.random.word()
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.name.firstName()
- fireEvent.input(input, { target: { value: nextValue } })
-
- await waitFor(() => {
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
- })
-
- test('Should update input number value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.datatype.number(),
- type: 'number'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.datatype.number()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input checkbox value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.datatype.boolean(),
- type: 'checkbox'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.datatype.number()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input range value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.datatype.number({ min: 0, max: 100 }),
- type: 'range'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.datatype.number()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input date value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.date.past(),
- type: 'date'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = new Date(faker.date.past()).toTimeString()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input time value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.date.past(),
- type: 'time'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = new Date(faker.date.past()).toTimeString()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input datetime-local value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.date.past(),
- type: 'datetime-local'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = new Date(faker.date.past()).toTimeString()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input month value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.date.past(),
- type: 'month'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = new Date(faker.date.past()).toTimeString()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input week value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.date.past(),
- type: 'week'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = new Date(faker.date.past()).toTimeString()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input color value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.internet.color(),
- type: 'color'
- })
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.internet.color()
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input radio value when dispatch input event', async () => {
- const mock = makeUseFormParamsMock({
- value: 'option-1'
- })
- const { hookState, sut } = makeRadioSut(mock)
- const input = sut.getByTestId('radio-2')
- const nextValue = 'option-2'
- fireEvent.click(input)
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input select value when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: 'option-1'
- })
- const { hookState, sut } = makeSelectSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = 'option-2'
- fireEvent.input(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-})
-
-describe('Tests useForm mode', () => {
- test('Should update input text value just when dispatch blur event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.lorem.words(),
- type: 'text',
- mode: 'onBlur'
- })
-
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.lorem.words()
- fireEvent.input(input, { target: { value: nextValue } })
- expect(hookState.state.values.inputName).toEqual(
- mock.hookParams.initialValues.inputName
- )
-
- fireEvent.blur(input)
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input text value just when dispatch input event', () => {
- const mock = makeUseFormParamsMock({
- value: faker.lorem.words(),
- type: 'text'
- })
-
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.lorem.words()
-
- fireEvent.blur(input, { target: { value: nextValue } })
- expect(hookState.state.values.inputName).toEqual(
- mock.hookParams.initialValues.inputName
- )
-
- fireEvent.input(input, { target: { value: nextValue } })
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-
- test('Should update input text value just 500ms after dispatch input event', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.lorem.words(),
- type: 'text',
- mode: 'debounced'
- })
-
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.lorem.words()
-
- fireEvent.input(input, { target: { value: nextValue } })
- expect(hookState.state.values.inputName).toEqual(
- mock.hookParams.initialValues.inputName
- )
-
- await waitFor(() => {
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
- })
-
- test('Should receive form value when submit form', async () => {
- const mock = makeUseFormParamsMock({
- value: faker.lorem.words(),
- type: 'text',
- mode: 'onSubmit'
- })
-
- const { hookState, sut } = makeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.lorem.words()
- const onSubmitParams = [
- {
- inputName: nextValue
- },
- true
- ]
-
- fireEvent.input(input, { target: { value: nextValue } })
- expect(hookState.state.values.inputName).toEqual(
- mock.hookParams.initialValues.inputName
- )
-
- fireEvent.submit(sut.getByTestId('form'))
-
- await waitFor(() => {
- expect(mock.onSubmit).toHaveBeenCalledWith(...onSubmitParams)
- })
- })
-
- test('Should change input value when run handleChange function', () => {
- const mock = makeUseFormParamsMock({
- value: faker.lorem.words(),
- type: 'text'
- })
-
- const { hookState, sut } = makeHandleChangeSut(mock)
- const input = sut.getByTestId(mock.inputParams.name)
- const nextValue = faker.lorem.words()
- fireEvent.change(input, { target: { value: nextValue } })
-
- expect(hookState.state.values.inputName).toEqual(nextValue)
- })
-})
diff --git a/__tests__/utils/makeSut.tsx b/__tests__/utils/makeSut.tsx
deleted file mode 100644
index 7aa2d21d..00000000
--- a/__tests__/utils/makeSut.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import { useForm } from '../../src/index'
-import { UseFormReturnType } from '../../src/types'
-
-export function makeSut({ hookParams, inputParams, onSubmit }: any) {
- const hookState: UseFormReturnType = Object.assign({})
-
- function InputComponent() {
- const { state, register, ...rest } = useForm(hookParams)
- Object.assign(hookState, { state, ...rest })
-
- return (
-
- )
- }
-
- const sut = render( )
-
- return { sut, hookState }
-}
-
-export function makeHandleChangeSut({
- hookParams,
- inputParams,
- onSubmit
-}: any) {
- const hookState: UseFormReturnType = Object.assign({})
-
- function InputComponent() {
- const { state, register, ...rest } = useForm(hookParams)
- Object.assign(hookState, { state, ...rest })
-
- return (
-
- )
- }
-
- const sut = render( )
-
- return { sut, hookState }
-}
-
-export function makeRadioSut({ hookParams, inputParams, onSubmit }: any) {
- const hookState: UseFormReturnType = Object.assign({})
-
- function InputComponent() {
- const { state, register, ...rest } = useForm(hookParams)
- Object.assign(hookState, { state, ...rest })
-
- return (
-
- )
- }
-
- const sut = render( )
-
- return { sut, hookState }
-}
-
-export function makeSelectSut({ hookParams, inputParams, onSubmit }: any) {
- const hookState: UseFormReturnType = Object.assign({})
-
- function InputComponent() {
- const { state, register, ...rest } = useForm(hookParams)
- Object.assign(hookState, { state, ...rest })
-
- return (
-
- )
- }
-
- const sut = render( )
-
- return { sut, hookState }
-}
-
-export function makeUseFormParamsMock({
- value = '',
- name = 'inputName',
- type = 'text',
- touched = false,
- error = '',
- mode = 'onChange'
-}: {
- value?: any
- name?: string
- type?: string
- touched?: boolean
- error?: string
- mode?: 'onChange' | 'onBlur' | 'debounced' | 'onSubmit'
-}) {
- return {
- hookParams: {
- initialValues: {
- inputName: value
- },
- initialTouched: {
- inputName: touched
- },
- initialErrors: {
- inputName: error
- },
- mode
- },
- onSubmit: jest.fn(),
- inputParams: {
- name,
- type
- }
- }
-}
diff --git a/__tests__/utils/tes-input-file.json b/__tests__/utils/tes-input-file.json
deleted file mode 100644
index 6bd2dd80..00000000
--- a/__tests__/utils/tes-input-file.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "test": "file.json"
-}
\ No newline at end of file
diff --git a/example/.gitignore b/example/.gitignore
index 587e4ec7..d451ff16 100644
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -1,3 +1,5 @@
node_modules
-.cache
-dist
\ No newline at end of file
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/example/babel.config.js b/example/babel.config.js
deleted file mode 100644
index 2dc03f52..00000000
--- a/example/babel.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
- module.exports = {
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react",
- "@babel/typescript",
- ],
- "plugins": [
- "@babel/proposal-class-properties",
- "@babel/proposal-object-rest-spread",
- "@babel/plugin-syntax-optional-chaining",
- "@babel/plugin-proposal-optional-chaining",
- "react-hot-loader/babel",
- "@babel/plugin-transform-runtime",
- "@babel/plugin-transform-classes"
- ]
-}
\ No newline at end of file
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 00000000..38f38611
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/example/package.json b/example/package.json
index 027ba7a5..f20ff80a 100644
--- a/example/package.json
+++ b/example/package.json
@@ -1,43 +1,25 @@
{
- "name": "playground",
- "version": "1.0.0",
- "main": "index.js",
- "license": "MIT",
+ "name": "react-create-form",
+ "version": "0.0.0",
"scripts": {
- "start": "webpack-dev-server --mode development"
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
},
"dependencies": {
- "@material-ui/core": "*",
- "bootstrap": "^4.6.0",
- "react-app-polyfill": "^1.0.0",
- "react-select": "^5.1.0",
- "yup": "^0.32.11"
- },
- "alias": {
- "react": "../node_modules/react",
- "react-dom": "../node_modules/react-dom/profiling",
- "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
+ "@types/react-datepicker": "^4.3.4",
+ "react": "^17.0.2",
+ "react-datepicker": "^4.6.0",
+ "react-dom": "^17.0.2",
+ "react-select": "^5.2.1",
+ "yup": "^0.27.0"
},
"devDependencies": {
- "@babel/core": "^7.10.5",
- "@babel/plugin-transform-classes": "^7.13.0",
- "@babel/plugin-transform-runtime": "^7.13.10",
- "@babel/preset-env": "^7.10.4",
- "@babel/preset-react": "^7.10.4",
- "@babel/preset-typescript": "^7.10.4",
- "@pmmmwh/react-refresh-webpack-plugin": "^0.3.3",
- "@types/react": "^16.9.11",
- "@types/react-dom": "^16.8.4",
- "babel-loader": "^8.2.2",
- "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
- "css-loader": "^5.2.6",
- "html-webpack-plugin": "^4.3.0",
- "react-hot-loader": "^4.13.0",
- "react-refresh": "^0.9.0",
- "style-loader": "^2.0.0",
- "typescript": "^4.2.2",
- "webpack": "^4.43.0",
- "webpack-cli": "^3.3.12",
- "webpack-dev-server": "^3.11.0"
+ "@types/react": "^17.0.33",
+ "@types/react-dom": "^17.0.10",
+ "@types/yup": "^0.29.13",
+ "@vitejs/plugin-react": "^1.0.7",
+ "typescript": "^4.4.4",
+ "vite": "^2.7.2"
}
}
diff --git a/example/public/index.html b/example/public/index.html
deleted file mode 100644
index 03a65e87..00000000
--- a/example/public/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
- Playground
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/src/App.css b/example/src/App.css
new file mode 100644
index 00000000..8da3fde6
--- /dev/null
+++ b/example/src/App.css
@@ -0,0 +1,42 @@
+.App {
+ text-align: center;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #282c34;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+button {
+ font-size: calc(10px + 2vmin);
+}
diff --git a/example/src/App.tsx b/example/src/App.tsx
new file mode 100644
index 00000000..de705611
--- /dev/null
+++ b/example/src/App.tsx
@@ -0,0 +1,218 @@
+import { useState } from 'react'
+import * as yup from 'yup'
+import './App.css'
+import { createForm, Wrapper } from '../../src'
+import Select from 'react-select'
+import DatePicker from 'react-datepicker'
+
+import 'react-datepicker/dist/react-datepicker.css'
+
+const validationSchema = yup.object().shape({
+ email: yup.string().email().required(),
+ password: yup.string().required(),
+ agree: yup.boolean().required(),
+ gender: yup.string().required()
+})
+
+function App() {
+ const [count, setCount] = useState(0)
+
+ return (
+
+
+ {/* */}
+
+ )
+}
+
+export default App
+
+const useLoginForm = createForm({
+ initialValues: {
+ email: 'juciano@juciano.com',
+ password: '1234567',
+ agree: true,
+ gender: 'masculine',
+ distance: 10,
+ location: {
+ city: '',
+ state: '',
+ zip: ''
+ },
+ select: {
+ label: 'Outro',
+ value: 'other'
+ },
+ date: null
+ },
+ validationSchema
+ // initialErrors: {
+ // email: 'E-mail ja esta sendo usado'
+ // }
+})
+
+const options = [
+ { value: 'masculine', label: 'Masculino' },
+ { value: 'feminine', label: 'Feminino' },
+ { value: 'other', label: 'Outro' }
+]
+
+function FormComponent() {
+ const {
+ state,
+ register,
+ form,
+ setFieldValue,
+ setFieldsValue,
+ reset,
+ resetValues,
+ resetTouched,
+ resetErrors,
+ setFieldsError,
+ setFieldsTouched,
+ handleSubmit
+ } = useLoginForm({
+ mode: 'onChange',
+ onChange: values => {
+ // console.log('onChange', values)
+ // return values
+ }
+ })
+
+ function handleEmail() {
+ setFieldValue('email', 'jose@jose.com')
+ }
+
+ function handleReset() {
+ reset()
+ }
+
+ function handleResetValues() {
+ resetValues()
+ }
+
+ function handleResetTouched() {
+ resetTouched()
+ }
+
+ function handleResetErrors() {
+ resetErrors()
+ }
+
+ function handleAllValues() {
+ setFieldsValue({
+ email: 'jose@jose.com',
+ password: '1234567891011121314151618',
+ gender: 'female',
+ agree: false,
+ distance: 10,
+ location: {
+ city: 'São Paulo',
+ state: 'SP',
+ zip: '01001-000'
+ },
+ select: {
+ value: 'masculine',
+ label: 'Masculino'
+ },
+ date: null
+ })
+
+ setFieldsTouched(state => ({
+ ...state,
+ email: true
+ }))
+ }
+
+ function handleSetErrors() {
+ setFieldsError(state => ({
+ ...state,
+ password: 'Senha deve conter no mínimo 8 caracteres'
+ }))
+ }
+
+ console.log('state', state)
+
+ return (
+
+ )
+}
+
+function FormComponent2() {
+ const { register, form } = useLoginForm()
+ return (
+
+
Form2
+
+
+ )
+}
diff --git a/example/src/Controlled/index.tsx b/example/src/Controlled/index.tsx
deleted file mode 100644
index d1765fb7..00000000
--- a/example/src/Controlled/index.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import React, { useEffect } from 'react'
-import { useForm } from '../../../src/hooks/useForm'
-import { useFormContext } from '../../../src/hooks/useContextForm'
-import { FormContextProvider } from '../../../src/core/contextForm'
-import Select from 'react-select'
-
-const options = [
- { value: 'chocolate', label: 'Chocolate' },
- { value: 'strawberry', label: 'Strawberry' },
- { value: 'vanilla', label: 'Vanilla' }
-]
-
-const Controlled: React.FC = () => {
- const { state, register, setFieldsValue, onSubmit, ...form } = useForm({
- initialValues: {
- name: 'juciano',
- email: 'juciano@juciano.com'
- // password: '123456',
- }
- })
-
- console.log(state)
-
- return (
-
-
-
- )
-}
-
-function InnerForm() {
- const { register, setFieldValue, state, setFieldTouched } = useFormContext()
-
- // console.log(form.state.values)
-
- return (
-
-
-
-
- Select
- setFieldTouched('select', true)}
- onChange={e => {
- setFieldValue('select', e)
- }}
- />
-
-
-
setFieldValue('name', 'juciano c barbosa')}
- >
- Change
-
-
- )
-}
-
-export default Controlled
diff --git a/example/src/favicon.svg b/example/src/favicon.svg
new file mode 100644
index 00000000..de4aeddc
--- /dev/null
+++ b/example/src/favicon.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/index.css b/example/src/index.css
index 5e36bff1..ec2585e8 100644
--- a/example/src/index.css
+++ b/example/src/index.css
@@ -1,18 +1,13 @@
-@import '../node_modules/bootstrap/dist/css/bootstrap.css';
-
body {
- background-color: #282c34;
- min-height: 100vh;
- font-size: calc(10px + 2vmin);
- color: white;
- }
-
- .text-error{
- color: red;
- font-size:14px;
- }
-
-
- .section{
- margin: 50px;
- }
\ No newline at end of file
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/example/src/index.tsx b/example/src/index.tsx
deleted file mode 100644
index cf3b4476..00000000
--- a/example/src/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import 'react-app-polyfill/ie11'
-import React from 'react'
-import ReactDOM from 'react-dom'
-import { useState } from 'react'
-import Controlled from './Controlled'
-import './index.css'
-
-const App = () => {
- const [date, setDate] = useState()
- return (
-
-
-
- )
-}
-
-ReactDOM.render( , document.getElementById('root'))
diff --git a/example/src/logo.svg b/example/src/logo.svg
new file mode 100644
index 00000000..6b60c104
--- /dev/null
+++ b/example/src/logo.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/example/src/main.tsx b/example/src/main.tsx
new file mode 100644
index 00000000..9d416461
--- /dev/null
+++ b/example/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import './index.css'
+import App from './App'
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+)
diff --git a/example/src/vite-env.d.ts b/example/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/example/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/example/tsconfig.json b/example/tsconfig.json
index 44f5b540..9f836599 100644
--- a/example/tsconfig.json
+++ b/example/tsconfig.json
@@ -1,24 +1,20 @@
{
"compilerOptions": {
- "allowSyntheticDefaultImports": false,
- "target": "es5",
- "module": "CommonJS",
- "jsx": "react",
- "moduleResolution": "node",
- "noImplicitAny": false,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "removeComments": true,
- "strictNullChecks": true,
- "preserveConstEnums": true,
- "sourceMap": true,
- "lib": [
- "es2015",
- "es2016",
- "dom"
- ],
- "types": [
- "node"
- ]
- }
-}
\ No newline at end of file
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": false,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["./src"]
+}
diff --git a/example/vite.config.ts b/example/vite.config.ts
new file mode 100644
index 00000000..01e1fa46
--- /dev/null
+++ b/example/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()]
+})
diff --git a/example/webpack.config.js b/example/webpack.config.js
deleted file mode 100644
index 2be9d342..00000000
--- a/example/webpack.config.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const path = require('path')
-const HtmlWebpackPlugin = require('html-webpack-plugin')
-const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
-
-const mapStyle = process.env.MAP_STYLE === 'true'
-const isDevelopment = process.env.NODE_ENV !== 'production'
-
-module.exports = {
- mode: isDevelopment ? 'development' : 'production',
- resolve: {
- extensions: ['.tsx', '.ts', '.js']
- },
- entry: path.resolve(__dirname, 'src', 'index.tsx'),
- module: {
- rules: [
- {
- test: /\.(js|ts|tsx)$/,
- exclude: /node_modules/,
- use: {
- loader: 'babel-loader'
- }
- },
- {
- test: /\.css$/,
- use: [
- { loader: 'style-loader' },
- { loader: mapStyle ? 'css-loader?sourceMap' : 'css-loader' }
- ]
- }
- ]
- },
- devServer: {
- contentBase: path.join(__dirname, 'public'),
- historyApiFallback: true,
- hot: true,
- port: 5000
- },
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: 'bundle.js'
- },
- plugins: [
- new HtmlWebpackPlugin({
- template: path.resolve(__dirname, 'public', 'index.html')
- }),
- isDevelopment && new ReactRefreshWebpackPlugin()
- ].filter(Boolean)
-}
diff --git a/package.json b/package.json
index 422e0849..5eb043af 100644
--- a/package.json
+++ b/package.json
@@ -119,7 +119,8 @@
"tslint-config-prettier": "^1.18.0",
"tslint-config-standard": "^9.0.0",
"typescript": "^4.3.5",
- "semantic-release": "^18.0.0"
+ "semantic-release": "^18.0.0",
+ "jest-each": "^27.4.0"
},
"dependencies": {
},
diff --git a/src/CreateForm.ts b/src/CreateForm.ts
new file mode 100644
index 00000000..39f36644
--- /dev/null
+++ b/src/CreateForm.ts
@@ -0,0 +1,406 @@
+import React from 'react'
+import { createStore } from './Store'
+import {
+ CreateFormArgs,
+ Errors,
+ EventChange,
+ Field,
+ HookArgs,
+ KeyValue,
+ State,
+ Touched
+} from './Types'
+import * as Dot from './ObjectUtils'
+import { extractRadioElements, isCheckbox, isRadio } from './FieldsUtils'
+import { debounce } from './Debounce'
+import { validate } from './Validate'
+import { StateChange } from '.'
+import {
+ InvalidArgumentException,
+ InvalidOperationException
+} from './Exception'
+
+const defaultValues = {
+ initialValues: {},
+ initialErrors: {},
+ initialTouched: {}
+}
+
+/**
+ * createForm function create a form Store and return a hook that can be used to manage the form state.
+ * @param args CreateFormArgs type that contains the initial values of form, initial errors of form, initial touched of form,
+ * @returns {function(*): *} a function that returns a hook that can be used to manage the form state.
+ **/
+export function createForm>(
+ args: T
+) {
+ const { initialValues, initialErrors, initialTouched, validationSchema } = {
+ ...defaultValues,
+ ...args
+ }
+
+ /**
+ * This is the store of the form,
+ * it is an object that contains the values of form,
+ * errors of form,
+ * touched of form.
+ **/
+ const $store = createStore({
+ values: initialValues,
+ errors: initialErrors,
+ touched: initialTouched,
+ isValid: Dot.isEmpty(initialErrors)
+ })
+
+ return (hookArgs?: HookArgs) => {
+ /**
+ * This is the reference of all native inputs of the form,
+ * in order to have the same reference of all inputs of the form.
+ **/
+ const inputsRefs = React.useRef>>({})
+
+ /**
+ * This is the state of the form,
+ * it is an object that contains the values of form,
+ * errors of form,
+ * touched of form.
+ **/
+ const [state, setState] = React.useState>(
+ $store.get()
+ )
+
+ /**
+ * Debounce mode is a mode that is used when the form is debounced,
+ **/
+ const setStateDebounced = React.useCallback(debounce(setState, 500), [])
+
+ /**
+ * This is the function that is used to set the state of the form, using debounce mode.
+ * Because we are using native events to update the input value consequently we have many events that are fired at the same time,
+ * so we need to debounce the state update to avoid the state to be updated many times.
+ * @param state
+ **/
+ const persistNextStateDebounced = React.useCallback(
+ debounce(persistNextState, 100),
+ []
+ )
+
+ /**
+ * Register a new input to the form,
+ * this function is called by the Input component.
+ * @param name the name of the input
+ **/
+ function register(name: string) {
+ if (!name) {
+ throw new InvalidArgumentException('Input name is required')
+ }
+
+ const ref = React.useRef(null)
+ inputsRefs.current[name] = ref
+
+ React.useEffect(() => {
+ if (ref.current) {
+ ref.current.name = name
+ return persistInitialValues(
+ name,
+ Dot.get(initialValues, name)
+ )
+ }
+ throw new InvalidOperationException(
+ 'your input is not rendered yet, or you have not provided a name to the input, or you have not using the register function in the Input component'
+ )
+ }, [ref])
+
+ React.useEffect(() => {
+ ref.current?.addEventListener('input', handleChange as any)
+ return () => {
+ ref.current?.removeEventListener('input', handleChange as any)
+ }
+ }, [])
+
+ React.useEffect(() => {
+ if (ref.current) {
+ ref.current.addEventListener('blur', handleBlur as any)
+ }
+ return () => {
+ ref.current?.removeEventListener('blur', handleBlur as any)
+ }
+ }, [])
+
+ return ref
+ }
+
+ /**
+ * This function will handle input events of the form,
+ * @param event the event that will be handled
+ **/
+ async function handleChange(event: EventChange) {
+ const { name, value, checked } = event.target
+ const nextValue =
+ event.detail !== undefined && (event as any).detail !== 0
+ ? event.detail
+ : value
+
+ if (isCheckbox(event.target as Field)) {
+ $store.patch(`values.${name}`, checked)
+ } else {
+ $store.patch(`values.${name}`, nextValue)
+ }
+
+ try {
+ await validate($store.getPropertyValue('values'), validationSchema)
+ $store.patch('isValid', true)
+ $store.patch('errors', {})
+ } catch (errors: any) {
+ $store.patch('isValid', false)
+ $store.patch('errors', errors)
+ }
+ hookArgs?.onChange?.($store.getPropertyValue('values'))
+ }
+
+ /**
+ * This function will handle blur events
+ * @param event the event that will be handled
+ **/
+ function handleBlur(event: React.FocusEvent) {
+ const { name } = event.target
+ $store.patch(`touched.${name}`, true)
+
+ if (hookArgs?.onBlur) {
+ hookArgs.onBlur(state.values)
+ }
+ }
+
+ /**
+ * This function will handle form submit
+ **/
+ function handleSubmit(
+ submit: (values: T['initialValues'], isValid: boolean) => void
+ ) {
+ if (typeof submit !== 'function') {
+ throw Error('Submit function is required')
+ }
+ /**
+ * This function will handle submit event
+ * @param event the event that will be handled
+ **/
+ return (event: React.FormEvent) => {
+ event.preventDefault()
+ const state = $store.get()
+ const { values, isValid } = state
+ setState(state)
+ submit(values, isValid)
+ }
+ }
+
+ /**
+ * Persist initial values in native fields
+ * @param values the values of the form
+ **/
+ function persistInitialValues(name: string, value: any) {
+ setFieldValue(name, value)
+ }
+
+ /**
+ * This function will set the value into input ref,
+ * @param name the name of the input
+ * @param value the value of the input
+ **/
+ function setFieldValue(name: string, value: any) {
+ const ref = inputsRefs.current[name]
+ if (ref && ref.current) {
+ ref.current.value = value
+ if (isCheckbox(ref.current)) {
+ ref.current.checked = value
+ } else if (isRadio(ref.current)) {
+ const radios = extractRadioElements(ref.current)
+ for (const radio of radios) {
+ radio.checked = radio.value === value
+ }
+ }
+ /**---- Trigger change event ----*/
+ ref.current.dispatchEvent(new Event('input'))
+ } else {
+ throw Error(
+ `Input with name '${name}' is not registered, verify the input name.`
+ )
+ }
+ }
+
+ /**
+ * This function will set all inputs value into the input elements,
+ * @param values the values of the form
+ **/
+ function setFieldsValue(next: StateChange) {
+ const nextValues =
+ //@ts-ignore
+ typeof next === 'function' ? next(state.values) : next
+ const names = Object.keys(inputsRefs.current)
+ try {
+ for (const name of names) {
+ const next = Dot.get(nextValues, name)
+ if (next !== undefined) {
+ setFieldValue(name, next)
+ }
+ }
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will set the error into the state of the form,
+ * @param name the name of the input
+ * @param error the error of the input
+ **/
+ function setFieldError(name: string, error: string) {
+ try {
+ $store.patch(`errors.${name}`, error)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will set all inputs error into the state of the form,
+ * @param errors the errors of the form
+ **/
+ function setFieldsError(next: StateChange>) {
+ const nextErrors =
+ typeof next === 'function' ? next($store.get().errors) : next
+ try {
+ $store.patch('errors', nextErrors)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will set the touched into the state of the form,
+ * @param name the name of the input
+ * @param touched the touched of the input
+ **/
+ function setFieldTouched(name: string, touched = true) {
+ try {
+ $store.patch(`touched.${name}`, touched)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will set all inputs touched into the state of the form,
+ * @param touched the touched of the form
+ **/
+ function setFieldsTouched(
+ next: StateChange>
+ ) {
+ const nextTouched =
+ typeof next === 'function' ? next($store.get().touched) : next
+ const names = Object.keys(inputsRefs.current)
+ try {
+ if (!nextTouched) {
+ for (const name of names) {
+ setFieldTouched(name)
+ }
+ }
+ $store.patch('touched', nextTouched)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will reset the form as initial state,
+ **/
+ function reset() {
+ setFieldsValue(initialValues as T['initialValues'])
+ setFieldsError(initialErrors as Errors)
+ setFieldsTouched(initialTouched as Touched)
+ }
+
+ /**
+ * This function will reset the form as initial values,
+ **/
+ function resetValues() {
+ setFieldsValue(initialValues as T['initialValues'])
+ }
+
+ /**
+ * This function will reset the form as initial errors,
+ **/
+ function resetErrors() {
+ $store.patch('errors', initialErrors as Errors)
+ }
+
+ /**
+ * This function will reset the form as initial touched,
+ **/
+ function resetTouched() {
+ $store.patch('touched', initialTouched as Touched)
+ }
+
+ /**
+ * This function will patch the state of the form, by setting the value of the input into form store,
+ * @param name the name of the input
+ * @param value the value of the input
+ **/
+ function setFieldStoreValue(name: string, value: any) {
+ try {
+ $store.patch(`values.${name}`, value)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ /**
+ * This function will patch the state of the form, by setting the touched of the input into form store,
+ * @param name the name of the input
+ * @param touched the touched of the input
+ **/
+ function setFieldStoreTouched(name: string, touched = true) {
+ try {
+ $store.patch(`touched.${name}`, touched)
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ function persistNextState(nextState: State) {
+ if (hookArgs?.mode === 'debounce') {
+ setStateDebounced(nextState)
+ } else if (hookArgs?.mode === 'onChange') {
+ setState(nextState)
+ }
+ }
+
+ /**
+ * Subscribe to the store to get the next state and update the form
+ **/
+ React.useEffect(() => {
+ const unsubscribe = $store.subscribe(persistNextStateDebounced)
+ return () => {
+ unsubscribe()
+ }
+ }, [])
+
+ return {
+ form: $store,
+ register,
+ setFieldValue,
+ setFieldsValue,
+ setFieldError,
+ setFieldsError,
+ setFieldTouched,
+ setFieldsTouched,
+ reset,
+ resetValues,
+ resetErrors,
+ resetTouched,
+ handleSubmit,
+ setFieldStoreTouched,
+ setFieldStoreValue,
+ state
+ }
+ }
+}
diff --git a/src/Debounce.ts b/src/Debounce.ts
new file mode 100644
index 00000000..456ef114
--- /dev/null
+++ b/src/Debounce.ts
@@ -0,0 +1,25 @@
+export function debounce(
+ this: TThis,
+ fn: TFn,
+ wait: number,
+ immediate?: boolean
+) {
+ let timeout: any
+
+ return (...args: Array) => {
+ const context = this
+
+ const later = () => {
+ timeout = null
+ if (!immediate) fn.apply(context, args)
+ }
+
+ const callNow = immediate && !timeout
+ clearTimeout(timeout)
+ timeout = setTimeout(later, wait)
+
+ if (callNow) {
+ fn.apply(context, args)
+ }
+ }
+}
diff --git a/src/Exception.ts b/src/Exception.ts
new file mode 100644
index 00000000..9eddceca
--- /dev/null
+++ b/src/Exception.ts
@@ -0,0 +1,19 @@
+export class Exception extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = this.constructor.name
+ Error.captureStackTrace(this, this.constructor)
+ }
+}
+
+export class InvalidArgumentException extends Exception {
+ constructor(message: string) {
+ super(message)
+ }
+}
+
+export class InvalidOperationException extends Exception {
+ constructor(message: string) {
+ super(message)
+ }
+}
diff --git a/src/FieldsUtils.ts b/src/FieldsUtils.ts
new file mode 100644
index 00000000..f870bed9
--- /dev/null
+++ b/src/FieldsUtils.ts
@@ -0,0 +1,38 @@
+import { Field, PrimitiveValue } from './Types'
+
+export function isCheckbox(field: Field): boolean {
+ return field.type === 'checkbox'
+}
+
+export function isRadio(field: Field): boolean {
+ return !!field?.querySelector('input[type="radio"]')
+}
+
+export function extractRadioElements(field: Field) {
+ return Array.from(field.querySelectorAll('input[type="radio"]'))
+}
+
+export function extractFieldValue(field: Field): PrimitiveValue {
+ if (isCheckbox(field)) {
+ return field.checked
+ } else if (isRadio(field)) {
+ return field.querySelector(
+ 'input[type="radio"]:checked'
+ )?.value
+ } else {
+ return field.value
+ }
+}
+
+export function setFieldValue(field: Field, value: PrimitiveValue) {
+ if (isCheckbox(field)) {
+ field.checked = value as boolean
+ } else if (isRadio(field)) {
+ const elements = extractRadioElements(field)
+ for (const element of elements) {
+ element.checked = element.value === value
+ }
+ } else {
+ field.value = value as string
+ }
+}
diff --git a/src/ObjectUtils.ts b/src/ObjectUtils.ts
new file mode 100644
index 00000000..75506667
--- /dev/null
+++ b/src/ObjectUtils.ts
@@ -0,0 +1,62 @@
+function propToPath(prop: any) {
+ return prop.replace(/["|']|\]/g, '').split(/\.|\[/)
+}
+
+export function set(
+ defaultObject: T,
+ prop: string,
+ value: any
+) {
+ const paths = propToPath(prop)
+
+ function setPropertyValue(object: Partial = {}, index: number) {
+ let clone: any = Object.assign({}, object)
+
+ if (paths.length > index) {
+ if (Array.isArray(object)) {
+ paths[index] = parseInt(paths[index])
+ clone = object.slice() as any
+ }
+ clone[paths[index]] = setPropertyValue(object[paths[index]], index + 1)
+
+ return clone
+ }
+ return value
+ }
+
+ return setPropertyValue(defaultObject, 0)
+}
+
+export function get(defaultObject: T, prop: string) {
+ const paths: Array = propToPath(prop)
+
+ function getPropertyValue(
+ object: { [k: string]: any } = {},
+ index: number
+ ): any {
+ const clone: any = Object.assign({}, object)
+ if (paths.length === index + 1) {
+ if (Array.isArray(clone[paths[index]])) {
+ return clone[paths[index]].slice()
+ } else if (typeof clone[paths[index]] === 'object') {
+ if (clone[paths[index]] === null) {
+ return null
+ }
+ return Object.assign({}, clone[paths[index]])
+ }
+ return clone[paths[index]]
+ }
+ return getPropertyValue(object[paths[index]], index + 1)
+ }
+
+ return getPropertyValue(defaultObject, 0)
+}
+
+export function isEmpty(obj: any) {
+ for (const key in obj) {
+ if (Object.hasOwnProperty.call(obj, key)) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/src/core/observable.ts b/src/Store.ts
similarity index 61%
rename from src/core/observable.ts
rename to src/Store.ts
index de15af38..dd39ccce 100644
--- a/src/core/observable.ts
+++ b/src/Store.ts
@@ -1,16 +1,13 @@
-import { State } from '../types'
-import * as dot from '../utils/dot-prop'
+import * as Dot from './ObjectUtils'
-type Subscribe = (e: State) => void
+type Subscribe = (e: TValues) => void
type Subscribers = Array>
-export function createState>(
- initialState: T = Object.assign({})
-) {
+export function createStore(initialState: T = Object.assign({})) {
let state = initialState
let subscribers: Subscribers = []
- function get(): State {
+ function get(): T {
return state
}
@@ -24,25 +21,28 @@ export function createState>(
function set(nextState: T) {
state = nextState
-
notify()
}
function patch(path: string, next: any) {
- const nextState = dot.set(state, path, next)
- state = nextState
- notify()
+ const nextState = Dot.set(state, path, next)
+ if (typeof nextState !== 'undefined') {
+ state = nextState
+ notify()
+ } else {
+ throw new Error(`The path '${path}' is not defined`)
+ }
}
function getPropertyValue(path: string) {
- return dot.get(state, path)
+ return Dot.get(state, path)
}
function getInitialPropertyValue(path: string) {
- return dot.get(initialState, path)
+ return Dot.get(initialState, path)
}
- function getInitialState(): State {
+ function getInitialState(): T {
return initialState
}
diff --git a/src/Types.ts b/src/Types.ts
new file mode 100644
index 00000000..780959ec
--- /dev/null
+++ b/src/Types.ts
@@ -0,0 +1,92 @@
+import React from 'react'
+
+/**
+ * state is one of properties that is returned by useForm hook, this object contains the current state of form when the form is controlled or debounced.
+ */
+export type State = {
+ values: T
+ errors: Errors
+ touched: Touched
+ isValid: boolean
+}
+
+/**
+ * Touched type represents a touched object that has all properties of a form values, when this properties is primitive type ww convert this in a boolean,
+ * otherwise if this an object we start again validating every properties.
+ */
+export type Touched = {
+ [k in keyof Values]?: Values[k] extends number | string | boolean | Date
+ ? boolean
+ : Values[k] extends Array
+ ? Touched[]
+ : Touched
+}
+
+/**
+ * Errors type represents a errors object that has all properties of a form values, when this properties is primitive type ww convert this in a string,
+ * otherwise if this an object we start again validating every properties.
+ */
+export type Errors = {
+ [k in keyof Values]?: Values[k] extends number | string | boolean | Date
+ ? string
+ : Values[k] extends Array
+ ? Errors[]
+ : Errors
+}
+
+/**
+ * useForm hook needs an object that describe and provide some properties like initial values of form, initial errors of form, initial touched of form,
+ * and needs know what kind of form, is Controlled, debounced is about that.
+ */
+export type CreateFormArgs = {
+ /** represents a initial value of form */
+ readonly initialValues?: T
+ /** represents a initial values of inputs errors */
+ readonly initialErrors?: Errors
+ /** represents a initial values of visited inputs */
+ readonly initialTouched?: Touched
+ /** validation schema provided by yup */
+ readonly validationSchema?: any //YupSchema
+}
+
+/**
+ * KeyValue type represents a key value object that has a key and a value.
+ * It' helpful to use this type when you want to use a key value object as a key of an object.
+ **/
+export type KeyValue = {
+ [k: string]: T
+}
+
+/**
+ * Inputs types
+ **/
+export type PrimitiveValue = string | number | boolean | Date | null | undefined
+
+export type Checkbox = HTMLInputElement
+
+export type Radio = HTMLInputElement
+
+export type Select = HTMLSelectElement
+
+export type Text = HTMLInputElement
+
+export type TextArea = HTMLTextAreaElement
+
+export type PrimitiveEvent = EventTarget & {
+ value: PrimitiveValue
+}
+
+export type Field = Checkbox & Radio & Select & Text & TextArea & PrimitiveEvent
+
+export type Mode = 'onChange' | 'onSubmit' | 'debounce'
+
+export type HookArgs = {
+ onChange?: (state: T) => T | void
+ onBlur?: (state: T) => T | void
+ onSubmit?: (state: T) => T | void
+ mode?: Mode
+}
+
+export type EventChange = React.ChangeEvent & CustomEvent
+
+export type StateChange = T | ((state: T) => T)
diff --git a/src/Validate.ts b/src/Validate.ts
new file mode 100644
index 00000000..24024af8
--- /dev/null
+++ b/src/Validate.ts
@@ -0,0 +1,23 @@
+import { Schema, ValidationError } from 'yup'
+import * as Dot from './ObjectUtils'
+
+export function makeDotNotation(str: string) {
+ return str.split('[').join('.').split(']').join('')
+}
+
+export function validate(
+ values: TValues,
+ validationSchema: Schema
+) {
+ return validationSchema
+ ?.validate(values, { abortEarly: false })
+ .then(() => {
+ return {}
+ })
+ .catch((e: ValidationError) => {
+ throw e.inner.reduce((acc, key) => {
+ const path = makeDotNotation(key.path)
+ return Dot.set(acc, path, key.message)
+ }, {})
+ })
+}
diff --git a/src/Wrapper.tsx b/src/Wrapper.tsx
new file mode 100644
index 00000000..13d32b5f
--- /dev/null
+++ b/src/Wrapper.tsx
@@ -0,0 +1,58 @@
+import React, { Fragment } from 'react'
+import { EventChange, Field } from '.'
+
+type Props = {
+ component: React.JSXElementConstructor
+} & any
+
+function WrapperComponent(
+ { component, ...rest }: Props,
+ ref: React.RefObject
+) {
+ const Component = component
+ const [value, setValue] = React.useState(null)
+
+ function handleOnChange(e: any) {
+ if (ref.current) {
+ ref.current.value = e
+ ref.current?.dispatchEvent(new CustomEvent('input', { detail: e }))
+ }
+ }
+
+ function handleOnBlur(e: any) {
+ if (ref.current) {
+ ref.current.value = e
+ ref.current?.dispatchEvent(new CustomEvent('blur', { detail: true }))
+ }
+ }
+
+ function handleEvent(e: EventChange) {
+ setValue(e.detail ?? e.target.value)
+ }
+
+ React.useEffect(() => {
+ if (ref.current) {
+ ref.current.addEventListener('input', handleEvent)
+ }
+ return () => {
+ if (ref.current) {
+ ref.current.removeEventListener('input', handleEvent)
+ }
+ }
+ }, [ref.current])
+
+ return (
+
+
+
+
+ )
+}
+
+export const Wrapper = React.forwardRef(WrapperComponent)
diff --git a/src/core/contextForm.ts b/src/core/contextForm.ts
deleted file mode 100644
index e757a3eb..00000000
--- a/src/core/contextForm.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { UseFormReturnType } from '../types'
-
-export const FormContext = React.createContext>(
- undefined as any
-)
-
-export const FormContextProvider = FormContext.Provider
-
-export const FormContextConsumer = FormContext.Consumer
diff --git a/src/hooks/useContextForm.ts b/src/hooks/useContextForm.ts
deleted file mode 100644
index ae9c52a4..00000000
--- a/src/hooks/useContextForm.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-import { FormContext } from '../core/contextForm'
-import { UseFormReturnType } from '../types'
-
-export function useFormContext() {
- const form = React.useContext(FormContext)
-
- return form as UseFormReturnType
-}
diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts
deleted file mode 100644
index 4e7b4a50..00000000
--- a/src/hooks/useForm.ts
+++ /dev/null
@@ -1,369 +0,0 @@
-import React from 'react'
-import { createState } from '../core/observable'
-import {
- Options,
- State,
- UseFormReturnType,
- RegisterReturn,
- Ref,
- SetType
-} from '../types'
-import {
- debounce,
- extractRadioButtons,
- getNextState,
- isCheckbox,
- isRadio,
- validate
-} from '../utils'
-import * as dot from '../utils/dot-prop'
-import { createException } from '../utils/exceptions'
-
-export function useForm>(
- initial?: TInitial
-): UseFormReturnType {
- const initialState = {
- values: initial?.initialValues,
- errors: initial?.initialErrors,
- touched: initial?.initialTouched
- }
- const { current: state$ } = React.useRef(
- createState(initialState as any)
- )
- const [state, setState] = React.useState>(
- initialState as any
- )
-
- const setStateDebounced = React.useCallback(debounce(setState, 500), [])
- const fields = React.useRef({})
-
- async function setValue(event: any) {
- const validationSchema = initial?.validationSchema
- const currentState = state$.get()
- const nextChecked = event.target.checked
- const nextValue = isNaN(event.target.value)
- ? event.target.value
- : parseInt(event.target.value)
-
- const nextState = dot.set(
- currentState,
- 'values.'.concat(event.target.name),
- isCheckbox(event.target.type) ? nextChecked : nextValue
- )
- const nextTouched = dot.set(
- currentState,
- 'touched.'.concat(event.target.name),
- true
- )
-
- try {
- if (validationSchema) {
- await validate(nextState.values, validationSchema)
- return state$.set({
- values: nextState.values,
- errors: {},
- touched: nextTouched.touched
- })
- }
-
- return state$.set({
- values: nextState.values,
- errors: nextState.errors,
- touched: nextTouched.touched
- })
- } catch (errors) {
- return state$.set({
- values: nextState.values,
- errors: errors,
- touched: nextTouched.touched
- })
- }
- }
-
- function setRefValue(ref: any, value: any) {
- if (isRadio(ref)) {
- const inputs = extractRadioButtons(ref) as HTMLInputElement[]
-
- for (const input of inputs) {
- input.checked = input.value === value
- }
- } else {
- fields.current[ref.current.name] = ref.current
- ref.current.value = value
- }
- }
-
- function register(name: string): RegisterReturn {
- if (!name) {
- createException(
- 'Register Function',
- 'argument field name is necessary'
- )
- }
- const ref = React.useRef[(null)
-
- React.useEffect(() => {
- ref.current?.addEventListener('input', setValue)
- return () => {
- ref.current?.removeEventListener('input', setValue)
- }
- }, [])
-
- React.useEffect(() => {
- if (initial?.mode === 'onBlur') {
- ref.current?.addEventListener('blur', handleOnBlur)
- return () => {
- ref.current?.removeEventListener('blur', handleOnBlur)
- }
- }
- return () => {}
- }, [])
-
- React.useEffect(() => {
- const value = state$.getPropertyValue('values.'.concat(name))
- if (typeof value === 'undefined') {
- return setRefValue(ref, '')
- }
- return setRefValue(ref, value)
- }, [])
-
- return {
- name,
- ref
- }
- }
-
- function handleOnBlur() {
- setState(state$.get())
- }
-
- function persistNextState(nextState: State]) {
- if (initial?.mode === 'debounced') {
- setStateDebounced(nextState)
- } else if (initial?.mode === 'onChange') {
- setState(nextState)
- }
- }
-
- React.useEffect(() => {
- const unsubscribe = state$.subscribe(persistNextState)
- return () => unsubscribe()
- }, [])
-
- function resetFieldsValue() {
- state$.set({ ...state, values: state$.getInitialState().values as any })
- for (const field in fields.current) {
- const path = 'values.'.concat(field)
- fields.current[field].value = state$.getInitialPropertyValue(path)
- }
- setState(state$.get())
- }
-
- function setFieldsValue(next: SetType) {
- if (!next) {
- createException('SetFieldsValue Function', 'argument next is required')
- }
- const values = getNextState(next, state?.values)
- state$.patch('values', values)
- for (const field in fields.current) {
- if (typeof fields.current[field] !== 'undefined') {
- fields.current[field].value = dot.get(values, field)
- }
- }
- setState(state$.get())
- }
-
- function setFieldValue(field: string, value: any) {
- if (!value || !field) {
- createException(
- 'SetFieldValue Function',
- 'argument field and value are required'
- )
- }
- const path = 'values.'.concat(field)
-
- state$.patch(path, value)
- if (fields.current[field]) {
- fields.current[field].value = value
- }
- setState(state$.get())
- }
-
- function handleChange(event: any) {
- if (!event.target.name) {
- createException(
- 'HandleChange Function',
- 'property input name is necessary'
- )
- }
- const path = 'values.'.concat(event.target.name)
- if (isCheckbox(event.target.type)) {
- state$.patch(path, event.target.checked)
- }
- state$.patch(path, event.target.value)
- setState(state$.get())
- }
-
- function resetFieldValue(field: string) {
- if (!field) {
- createException(
- 'ResetFieldValue Function',
- 'argument field is necessary'
- )
- }
- const path = 'values.'.concat(field)
- state$.patch(path, state$.getInitialPropertyValue(path))
- fields.current[field].value = state$.getInitialPropertyValue(path)
- setState(state$.get())
- }
-
- function setFieldsError(next: SetType) {
- if (!next) {
- createException(
- 'SetFieldsError Function',
- 'argument next is necessary'
- )
- }
- const errors = getNextState(next, state?.errors)
- state$.patch('errors', errors)
- setState(state$.get())
- }
-
- function setFieldError(field: string, error: string) {
- if (!error || !field) {
- createException(
- 'SetFieldError Function',
- 'argument field and error are necessary'
- )
- }
- const path = 'errors.'.concat(field)
- state$.patch(path, error)
- setState(state$.get())
- }
-
- function resetFieldError(field: string) {
- if (!field) {
- createException('`resetFieldError()` - field is necessary')
- }
- const path = 'errors.'.concat(field)
- state$.patch(path, state$.getInitialPropertyValue(path))
- setState(state$.get())
- }
-
- function resetFieldsError() {
- state$.patch('errors', state$.getInitialState().errors)
- setState(state$.get())
- }
-
- function setFieldsTouched(next: SetType) {
- if (!next) {
- return makeAllTouched()
- }
- const touched = getNextState(next, state?.values)
- state$.patch('touched', touched)
- setState(state$.get())
- }
-
- function resetFieldsTouched() {
- state$.patch('touched', state$.getInitialState().touched)
- setState(state$.get())
- }
-
- function setFieldTouched(field: string) {
- if (!field) {
- createException(
- 'SetFieldTouched Function',
- 'name is necessary to set field as touched'
- )
- }
- state$.patch('touched.'.concat(field), true)
- setState(state$.get())
- }
-
- function resetFieldTouched(field: string) {
- if (!field) {
- createException(
- 'ResetFieldTouched Function',
- 'Field argument is necessary to reset a field'
- )
- }
- state$.patch('touched.'.concat(field), false)
- setState(state$.get())
- }
-
- function setForm(next: SetType>) {
- if (!next) {
- state$.set(state$.getInitialState())
- }
- const nextState = getNextState(next, state)
-
- setFieldsValue(nextState.values)
- setFieldsError(nextState.errors)
- setFieldsTouched(nextState.touched)
- }
-
- function resetForm() {
- setFieldsValue(state$.getInitialState().values)
- setFieldsError(state$.getInitialState().errors)
- setFieldsTouched(state$.getInitialState().touched)
- }
-
- function makeAllTouched() {
- for (const field in fields.current) {
- setFieldTouched(field)
- }
- }
-
- function onSubmit(
- fn: (values: TInitial['initialValues'], isValid: boolean) => void
- ) {
- if (!fn) {
- createException(
- 'OnSubmit Function',
- 'callback function is not defined'
- )
- }
- return async (e: React.BaseSyntheticEvent) => {
- e.preventDefault()
- const values = state$.getPropertyValue('values')
- try {
- if (initial?.validationSchema) {
- await validate(values, initial.validationSchema)
- }
- fn(values, true)
- } catch (errors) {
- fn(values, false)
- state$.set({
- ...state,
- errors,
- touched: makeAllTouched()
- })
- }
- }
- }
-
- return {
- state,
- register,
-
- setFieldsTouched,
- resetFieldsTouched,
- setFieldTouched,
- resetFieldTouched,
-
- setFieldsError,
- resetFieldsError,
- setFieldError,
- resetFieldError,
-
- setFieldsValue,
- resetFieldsValue,
- setFieldValue,
- resetFieldValue,
- handleChange,
-
- resetForm,
- setForm,
- onSubmit
- }
-}
diff --git a/src/hooks/useValidation.ts b/src/hooks/useValidation.ts
deleted file mode 100644
index eaf95192..00000000
--- a/src/hooks/useValidation.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react'
-import { ValidationError, Schema as YupSchema } from 'yup'
-import * as dot from './../utils/dot-prop'
-import { makeDotNotation } from '../utils'
-
-export function useValidation<
- TValues extends {},
- Schema extends YupSchema
->(values: TValues, schema?: Schema) {
- const [errors, setErrors] = React.useState({} as TValues)
-
- const validate = React.useCallback(() => {
- schema
- ?.validate(values, { abortEarly: false })
- .then(() => {
- setErrors({} as TValues)
- })
- .catch((e: ValidationError) => {
- let errors = {}
- e.inner.forEach(key => {
- const path = makeDotNotation(key.path)
-
- errors = dot.set(errors, path, key.message)
- })
- setErrors({ ...errors } as TValues)
- })
- }, [schema, values])
-
- React.useEffect(() => {
- validate()
- }, [validate])
-
- return { errors, isValid: Object.keys(errors).length === 0 }
-}
diff --git a/src/index.ts b/src/index.ts
index f61eb4c9..72566401 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,3 @@
-export * from './hooks/useForm'
-export * from './hooks/useValidation'
-export * from './core/contextForm'
-export * from './hooks/useContextForm'
+export * from './CreateForm'
+export * from './Types'
+export * from './Wrapper'
diff --git a/src/types/index.ts b/src/types/index.ts
deleted file mode 100644
index 5d9b8735..00000000
--- a/src/types/index.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import React from 'react'
-import { Schema as YupSchema } from 'yup'
-
-export type SetType = ((value: T) => T) | T
-
-/**
- * Input reference is a union with all kinds of native inputs.
- */
-export type Ref = HTMLInputElement & HTMLSelectElement & HTMLTextAreaElement
-
-/**
- * This is the type of Register function, we just need the name and the reference of input
- */
-export type RegisterReturn = {
- name: string
- ref: React.RefObject[
-}
-
-/**
- * inputs reference is a type that of an object that has all native input in a form.
- */
-export type InputsRef = { [path: string]: Ref }
-
-/**
- * Touched type represents a touched object that has all properties of a form values, when this properties is primitive type ww convert this in a boolean,
- * otherwise if this an object we start again validating every properties.
- */
-export type Touched] = {
- [k in keyof Values]?: Values[k] extends number | string | boolean | Date
- ? boolean
- : Values[k] extends Array
- ? Touched[]
- : Touched
-}
-
-/**
- * Errors type represents a errors object that has all properties of a form values, when this properties is primitive type ww convert this in a string,
- * otherwise if this an object we start again validating every properties.
- */
-export type Errors = {
- [k in keyof Values]?: Values[k] extends number | string | boolean | Date
- ? string
- : Values[k] extends Array
- ? Errors[]
- : Errors
-}
-
-/**
- * useForm hook needs an object that describe and provide some properties like initial values of form, initial errors of form, initial touched of form,
- * and needs know what kind of form, is Controlled, debounced is about that.
- */
-export type Options = {
- /** represents a initial value of form */
- readonly initialValues?: T
- /** represents a initial values of inputs errors */
- readonly initialErrors?: Errors
- /** represents a initial values of visited inputs */
- readonly initialTouched?: Touched
- /** represents the why that the form will works */
- readonly mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'debounced'
- /** validation schema provided by yup */
- readonly validationSchema?: YupSchema
- /** watch every change in useForm even if is a uncontrolled form */
- readonly watch?: (e: T) => void
-}
-
-/**
- * state is one of properties that is returned by useForm hook, this object contains the current state of form when the form is controlled or debounced.
- */
-export type State = {
- values: T
- errors: Errors
- touched: Touched
-}
-
-/**
- * contains all properties of a object that useForm return.
- */
-export type UseFormReturnType = {
- /** set a new state in a form,(values, errors and touched) */
- setForm: (next: ChangeState>) => void
- /** reset all form state (values, errors and touched) */
- resetForm: () => void
-
- /** set fields value, change just field values */
- setFieldsValue: (next: ChangeState) => void
- /** set a value in a specific field */
- setFieldValue: (path: Paths, value: any) => void
- /** reset all field values */
- resetFieldsValue: () => void
- /** reset the value of a specific field */
- resetFieldValue: (path: Paths) => void
- /** handle input changes */
- handleChange: (e: React.ChangeEvent[) => void
-
- /** set all fields as touched */
- setFieldsTouched: (next: ChangeState]>) => void
- /** set specif field as touched */
- setFieldTouched: (path: Paths, value: boolean) => void
- /** reset all fields touched */
- resetFieldsTouched: () => void
- /** reset a specific touched value*/
- resetFieldTouched: (path: Paths) => void
-
- /** set an error in a specific field */
- setFieldError: (path: Paths, error: any) => void
- /** set errors in all fields */
- setFieldsError: (next: ChangeState>) => void
- /** reset specific field error */
- resetFieldError: (path: Paths) => void
- /** reset all fields error */
- resetFieldsError: () => void
-
- /** the state of form */
- state: State
- /** this function register a input */
- register: Register
- /** ran when the event submit is dispatched */
- onSubmit: (fn: (values: T, isValid: boolean) => void) => HandleSubmit
-}
-
-/** this function register a input */
-type Register = (path: string) => RegisterReturn
-
-/** abstraction of react event change */
-export type Change = React.ChangeEvent
-
-type ChangeState = T | ((state: T) => T)
-/** ran when the event submit is dispatched */
-type HandleSubmit = (e: React.BaseSyntheticEvent) => Promise
-
-/** paths are the parameter that register function receives, that type needs more improvements */
-export type Paths = keyof T | string
-
-// type Prev = [
-// never,
-// 0,
-// 1,
-// 2,
-// 3,
-// 4,
-// 5,
-// 6,
-// 7,
-// 8,
-// 9,
-// 10,
-// 11,
-// 12,
-// 13,
-// 14,
-// 15,
-// 16,
-// 17,
-// 18,
-// 19,
-// 20,
-// ...0[]
-// ]
-
-// type Join = K extends string | number
-// ? P extends string | number
-// ? `${K}${'' extends P ? '' : '.'}${P}`
-// : never
-// : never
-
-// export type Paths = [D] extends [never]
-// ? never
-// : T extends object
-// ? {
-// [K in keyof T]-?: K extends string | number
-// ? `${K}` | Join>
-// : never
-// }[keyof T]
-// : ''
diff --git a/src/utils/dot-prop.ts b/src/utils/dot-prop.ts
deleted file mode 100644
index 524ed3a5..00000000
--- a/src/utils/dot-prop.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-function propToPath(prop: any) {
- return prop.replace(/["|']|\]/g, '').split(/\.|\[/)
-}
-
-function isPrimitive(value: any) {
- return (
- typeof value === 'string' ||
- typeof value === 'number' ||
- typeof value === 'boolean'
- )
-}
-
-export function set(
- defaultObject: T,
- prop: string,
- value: any
-) {
- const paths = propToPath(prop)
-
- function setPropertyValue(object: Partial = {}, index: number) {
- let clone = Object.assign({}, object)
-
- if (paths.length > index) {
- if (Array.isArray(object)) {
- paths[index] = parseInt(paths[index])
- clone = object.slice() as any
- }
- clone[paths[index]] = setPropertyValue(object[paths[index]], index + 1)
-
- return clone
- }
- return value
- }
-
- return setPropertyValue(defaultObject, 0)
-}
-
-export function del(defaultObject: T, prop: string) {
- const paths = propToPath(prop)
-
- function deletePropertyValue(object: object, index: number) {
- let clone: any = Object.assign({}, object)
-
- if (paths.length > index) {
- if (Array.isArray(object)) {
- paths[index] = parseInt(paths[index])
- clone = object.slice()
- clone.splice(paths[index], 1)
- return clone
- }
- const result = deletePropertyValue(object[paths[index]], index + 1)
- typeof result === 'undefined'
- ? delete clone[paths[index]]
- : (clone[paths[index]] = result)
-
- return clone
- }
- return undefined
- }
-
- return deletePropertyValue(defaultObject, 0)
-}
-
-export function get(defaultObject: T, prop: string) {
- const paths = propToPath(prop)
-
- function getPropertyValue(object: object = {}, index: number): any {
- const clone = Object.assign({}, object)
- if (paths.length === index + 1) {
- if (Array.isArray(clone[paths[index]])) {
- return clone[paths[index]].slice()
- } else if (typeof clone[paths[index]] === 'object') {
- if (clone[paths[index]] === null) {
- return null
- }
- return Object.assign({}, clone[paths[index]])
- }
- return clone[paths[index]]
- }
- return getPropertyValue(object[paths[index]], index + 1)
- }
-
- return getPropertyValue(defaultObject, 0)
-}
-
-export function merge(
- defaultObject: T,
- prop: string,
- value: any
-) {
- const targetValue = get(defaultObject, prop)
- if (typeof targetValue === 'undefined' || isPrimitive(value)) {
- throw new Error('Target value is undefine, verify your property path')
- }
-
- if (Array.isArray(value)) {
- if (!Array.isArray(targetValue)) {
- throw new Error('The bot values should be arrays')
- }
- const resultValue = targetValue.concat(value)
- return set(defaultObject, prop, resultValue)
- }
-
- const resultValue = Object.assign(targetValue, value)
-
- return set(defaultObject, prop, resultValue)
-}
diff --git a/src/utils/exceptions.ts b/src/utils/exceptions.ts
deleted file mode 100644
index 0b27eaff..00000000
--- a/src/utils/exceptions.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export function createException(...args: any[]): Error {
- throw new Exception(args[0], args[1])
-}
-
-export class Exception extends Error {
- constructor(name: string, message: string) {
- super(message)
- this.name = name
- this.message = message
- }
-}
diff --git a/src/utils/index.ts b/src/utils/index.ts
deleted file mode 100644
index 5c95a00c..00000000
--- a/src/utils/index.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react'
-import { Schema, ValidationError } from 'yup'
-import * as dot from './dot-prop'
-
-export const isRadio = (ref: React.RefObject) => {
- return ref.current?.querySelector('input[type="radio"]')
-}
-
-export function extractRadioButtons(ref: React.RefObject) {
- return Array.from(ref.current?.querySelectorAll('input[type="radio"]') || [])
-}
-
-export const isCheckbox = (type: string) => type === 'checkbox'
-
-export function debounce(
- this: TThis,
- fn: TFn,
- wait: number,
- immediate?: boolean
-) {
- let timeout: any
-
- return (...args: Array) => {
- const context = this
-
- const later = () => {
- timeout = null
- if (!immediate) fn.apply(context, args)
- }
-
- const callNow = immediate && !timeout
- clearTimeout(timeout)
- timeout = setTimeout(later, wait)
-
- if (callNow) {
- fn.apply(context, args)
- }
- }
-}
-
-export function makeDotNotation(str: string) {
- return str.split('[').join('.').split(']').join('')
-}
-
-export function getNextState(next: any, state: any) {
- const nextState = typeof next === 'function' ? next(state) : next
- return nextState
-}
-
-export function validate(
- values: TValues,
- validationSchema: Schema
-) {
- return validationSchema
- ?.validate(values, { abortEarly: false })
- .then(() => {
- return {}
- })
- .catch((e: ValidationError) => {
- throw e.inner.reduce((acc, key) => {
- const path = makeDotNotation(key.path)
- return dot.set(acc, path, key.message)
- }, {})
- })
-}
diff --git a/test/CreateForm.test.tsx b/test/CreateForm.test.tsx
new file mode 100644
index 00000000..ebb33ad9
--- /dev/null
+++ b/test/CreateForm.test.tsx
@@ -0,0 +1,331 @@
+import React from 'react'
+import each from 'jest-each'
+import faker from 'faker'
+import { createForm } from './../src/CreateForm'
+import { CreateFormArgs } from '../src/Types'
+import { renderHook } from '@testing-library/react-hooks'
+import { waitFor, render, fireEvent } from '@testing-library/react'
+
+function makeSut(args: CreateFormArgs = {}, mode = 'onChange' as any) {
+ const state = {}
+
+ const spy = jest.fn()
+ const useForm = createForm(args)
+
+ const { result: sut } = renderHook(() =>
+ useForm({ mode, onChange: spy, onBlur: spy, onSubmit: spy })
+ )
+ function Component() {
+ const form = sut.current
+ Object.assign(state, form)
+
+ return (
+
+ )
+ }
+
+ const element = render( )
+
+ return {
+ element,
+ spy,
+ sut
+ }
+}
+
+function makeMockedValues() {
+ return {
+ name: faker.name.firstName(),
+ email: faker.internet.email(),
+ password: faker.internet.password()
+ }
+}
+
+describe('CreateForm', () => {
+ each(['onChange', 'debounce']).it(
+ 'Should init the hook with the initial properties - [%s] mode',
+ mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ expect(form.sut.current.state.values).toEqual(initialValues)
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldValue - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newValue = faker.name.findName()
+ form.sut.current.setFieldValue('name', newValue)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.values.name).toEqual(newValue)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldError - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newError = faker.name.findName()
+ form.sut.current.setFieldError('name', newError)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.errors.name).toEqual(newError)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldTouched - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ form.sut.current.setFieldTouched('name', true)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.touched.name).toEqual(true)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldsValue - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const newValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ form.sut.current.setFieldsValue(newValues)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.values).toEqual(newValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldsError - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newErrors = makeMockedValues()
+ form.sut.current.setFieldsError(newErrors)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.errors).toEqual(newErrors)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run setFieldsTouched - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newTouched = {
+ name: true,
+ email: true,
+ password: true
+ }
+ form.sut.current.setFieldsTouched(newTouched)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.touched).toEqual(newTouched)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run resetErrors - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newErrors = makeMockedValues()
+ form.sut.current.setFieldsError(newErrors)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.errors).toEqual(newErrors)
+ })
+ form.sut.current.resetErrors()
+ await waitFor(() => {
+ expect(form.sut.current.state.errors).toEqual({})
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run resetValues - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newValues = makeMockedValues()
+ form.sut.current.setFieldsValue(newValues)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.values).toEqual(newValues)
+ })
+ form.sut.current.resetValues()
+ await waitFor(() => {
+ expect(form.sut.current.state.values).toEqual(initialValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run resetTouched - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newTouched = {
+ name: true,
+ email: true,
+ password: true
+ }
+ form.sut.current.setFieldsTouched(newTouched)
+ await waitFor(() => {
+ expect(form.sut.current.state.touched).toEqual(newTouched)
+ })
+
+ form.sut.current.resetTouched()
+
+ await waitFor(() => {
+ expect(form.sut.current.state.touched).toEqual({})
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update the hook when run reset - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const newValues = makeMockedValues()
+ await waitFor(() => {
+ form.sut.current.setFieldsValue(newValues)
+ })
+ form.sut.current.reset()
+
+ await waitFor(() => {
+ expect(form.sut.current.state.values).toEqual(initialValues)
+ expect(form.sut.current.state.touched).toEqual({})
+ expect(form.sut.current.state.errors).toEqual({})
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'SHould call handleSubmit function when run onSubmit - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const submitButton = form.element.getByTestId('submit')
+
+ fireEvent.click(submitButton)
+ // since we aren't passing any validation we assume the form is valid
+ const submittedValues = [initialValues, true]
+
+ await waitFor(() => {
+ expect(form.spy).toHaveBeenCalledWith(...submittedValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should call handleSubmit function when run onSubmit with errors - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const initialErrors = {
+ name: faker.name.findName()
+ }
+
+ const form = makeSut({ initialValues, initialErrors }, mode)
+ const submitButton = form.element.getByTestId('submit')
+
+ fireEvent.click(submitButton)
+ // since we are passing an error validation we assume the form is invalid
+ const submittedValues = [initialValues, false]
+
+ await waitFor(() => {
+ expect(form.spy).toHaveBeenCalledWith(...submittedValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should call onChange function when any change event happens - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const input = form.element.getByTestId('name')
+ const nextValue = faker.name.findName()
+ const nextValues = {
+ ...initialValues,
+ name: nextValue
+ }
+
+ fireEvent.input(input, { target: { value: nextValue } })
+
+ await waitFor(() => {
+ expect(form.spy).toHaveBeenCalledWith(nextValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should call onBlur function when any blur event happens - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const input = form.element.getByTestId('name')
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(form.spy).toHaveBeenCalledWith(initialValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update hook state when a change event happens - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const input = form.element.getByTestId('name')
+ const nextValue = faker.name.findName()
+ const nextValues = {
+ ...initialValues,
+ name: nextValue
+ }
+
+ fireEvent.input(input, { target: { value: nextValue } })
+
+ await waitFor(() => {
+ expect(form.sut.current.state.values).toEqual(nextValues)
+ })
+ }
+ )
+
+ each(['onChange', 'debounce']).it(
+ 'Should update hook state when a blur event happens - [%s] mode',
+ async mode => {
+ const initialValues = makeMockedValues()
+ const form = makeSut({ initialValues }, mode)
+ const input = form.element.getByTestId('name')
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(form.sut.current.state.touched).toEqual({ name: true })
+ })
+ }
+ )
+})
diff --git a/test/ObjectUtils.test.ts b/test/ObjectUtils.test.ts
new file mode 100644
index 00000000..ee1efbb4
--- /dev/null
+++ b/test/ObjectUtils.test.ts
@@ -0,0 +1,61 @@
+import * as Dot from './../src/ObjectUtils'
+
+describe('Dot set', () => {
+ it('Should set a value', () => {
+ const obj = (value: string) => ({ foo: value })
+ const newValue = 'baz'
+ const newObj = Dot.set(obj('bar'), 'foo', newValue)
+ expect(newObj).toEqual(obj(newValue))
+ })
+
+ it('Should set a value in an array', () => {
+ const obj = (value: string) => ({ foo: [value] })
+ const newValue = 'bar'
+ const newObj = Dot.set(obj('bar'), 'foo[0]', newValue)
+ expect(newObj).toEqual(obj(newValue))
+ })
+
+ it('Should set a value in an array with a number', () => {
+ const newValue = 'baz'
+ const newObj = Dot.set({ foo: [] }, 'foo.1', newValue)
+ expect(newObj).toEqual({ foo: [undefined, newValue] })
+ })
+
+ it('Should set a value in a nested object', () => {
+ const newValue = 'baz'
+ const newObj = Dot.set({ foo: { bar: 'bar' } }, 'foo.bar', newValue)
+ expect(newObj).toEqual({ foo: { bar: newValue } })
+ })
+})
+
+describe('Dot get', () => {
+ it('Should get a value', () => {
+ const obj = { foo: 'bar' }
+ expect(Dot.get(obj, 'foo')).toEqual('bar')
+ })
+
+ it('Should get a value in an array', () => {
+ const obj = { foo: ['bar'] }
+ expect(Dot.get(obj, 'foo[0]')).toEqual('bar')
+ })
+
+ it('Should get a value in an array with a number', () => {
+ const obj = { foo: [undefined, 'bar'] }
+ expect(Dot.get(obj, 'foo.1')).toEqual('bar')
+ })
+
+ it('Should get a value in a nested object', () => {
+ const obj = { foo: { bar: 'bar' } }
+ expect(Dot.get(obj, 'foo.bar')).toEqual('bar')
+ })
+
+ it('Should return undefined when property does not exist', () => {
+ const obj = { foo: 'bar' }
+ expect(Dot.get(obj, 'bar')).toEqual(undefined)
+ })
+
+ it('Should return undefined when property does not exist in an array', () => {
+ const obj = { foo: ['bar'] }
+ expect(Dot.get(obj, 'foo[1]')).toEqual(undefined)
+ })
+})
diff --git a/test/Store.test.ts b/test/Store.test.ts
new file mode 100644
index 00000000..4268a03d
--- /dev/null
+++ b/test/Store.test.ts
@@ -0,0 +1,66 @@
+import { createStore } from '../src/Store'
+
+function makeSut(state = {}) {
+ const spy = jest.fn()
+ const sut = createStore(state)
+
+ return {
+ sut,
+ spy
+ }
+}
+
+describe('Store', () => {
+ it('Should set initial state when createStore is called', () => {
+ const initialState = {
+ foo: 'bar'
+ }
+ const { sut } = makeSut(initialState)
+ expect(sut.get()).toEqual(initialState)
+ })
+
+ it('Should set a new state when set is called', () => {
+ const { sut } = makeSut()
+ const newState = {
+ foo: 'bar'
+ }
+ sut.set(newState)
+ expect(sut.get()).toEqual(newState)
+ })
+
+ it('Should call subscribers when set is called', () => {
+ const { sut, spy } = makeSut()
+ sut.subscribe(spy)
+ sut.set({ foo: 'bar' })
+ expect(spy).toHaveBeenCalledWith({ foo: 'bar' })
+ })
+
+ it('Should patch a state when patch is called', () => {
+ const { sut } = makeSut()
+ sut.patch('foo', 'bar')
+ expect(sut.get()).toEqual({ foo: 'bar' })
+ })
+
+ it('Should call subscribers when patch is called', () => {
+ const { sut, spy } = makeSut()
+ sut.subscribe(spy)
+ sut.patch('foo', 'bar')
+ expect(spy).toHaveBeenCalledWith({ foo: 'bar' })
+ })
+
+ it('Should get a property value when getPropertyValue is called', () => {
+ const { sut } = makeSut()
+ sut.patch('foo', 'bar')
+ expect(sut.getPropertyValue('foo')).toEqual('bar')
+ })
+
+ it('Should get an initial property value when getInitialPropertyValue is called', () => {
+ const { sut } = makeSut()
+ expect(sut.getInitialPropertyValue('foo')).toEqual(undefined)
+ })
+
+ it('Should get an initial state when getInitialState is called', () => {
+ const { sut } = makeSut()
+ expect(sut.getInitialState()).toEqual({})
+ })
+})
diff --git a/test/Wrapper.test.tsx b/test/Wrapper.test.tsx
new file mode 100644
index 00000000..9ecd4327
--- /dev/null
+++ b/test/Wrapper.test.tsx
@@ -0,0 +1,63 @@
+import React from 'react'
+import { render, fireEvent, waitFor } from '@testing-library/react'
+import faker from 'faker'
+import { Wrapper } from '../src/Wrapper'
+import { createForm } from '../src'
+import { renderHook } from '@testing-library/react-hooks'
+
+function makeSut() {
+ const sut = render( )
+ return {
+ sut
+ }
+}
+
+function Component(props: any) {
+ function handleOnChange(e: React.ChangeEvent) {
+ props.onChange(e.target.value)
+ }
+
+ return (
+
+
+
+ )
+}
+
+const value = faker.random.word()
+const useForm = createForm({
+ initialValues: {
+ name: value
+ }
+})
+
+function Setup() {
+ const form = useForm()
+
+ return
+}
+
+describe('Wrapper', () => {
+ it('Should render children with the value', () => {
+ const { sut } = makeSut()
+ const input = sut.getByTestId('name') as HTMLInputElement
+ expect(input.value).toBe(value)
+ })
+
+ it('Should set the custom input value', async () => {
+ const form = renderHook(() => useForm({ mode: 'onChange' }))
+ const { sut } = makeSut()
+ const input = sut.getByTestId('name') as HTMLInputElement
+ const nextValue = faker.random.word()
+ fireEvent.change(input, { target: { value: nextValue } })
+
+ await waitFor(() => {
+ expect(form.result.current.state.values.name).toBe(nextValue)
+ })
+ })
+})