From f30d58d59d53241ac0375d3e1780929702f15b76 Mon Sep 17 00:00:00 2001
From: Waseem Dahman
Date: Sat, 23 Feb 2019 21:14:31 -0500
Subject: [PATCH] add global event handlers (#19)
* add global event handlers
* update types
* add tests
* rename types
* Update README.md
* update README.md
* update types
* add types for input.textarea
---
README.md | 113 ++++++++++++++++++++++++++++++++++++--
src/index.d.ts | 20 ++++++-
src/useFormState.js | 27 +++++++--
test/test-utils.js | 14 +++--
test/useFormState.test.js | 37 +++++++++++++
5 files changed, 195 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 7c30911..3330fe0 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,31 @@
+
+📖 Table of Contents
+
+
+- [Motivation](#motivation)
+- [Getting Started](#getting-started)
+- [Examples](#examples)
+ - [Basic Usage](#basic-usage)
+ - [Initial State](#initial-state)
+ - [Global Handlers](#global-handlers)
+ - [Without Using a `
` Element](#without-using-a-form--element)
+- [API](#api)
+ - [`initialState`](#initialstate)
+ - [`formOptions`](#formoptions)
+ - [`formOptions.onBlur`](#formoptionsonblur)
+ - [`formOptions.onChange`](#formoptionsonchange)
+ - [`formOptions.onTouched`](#formoptionsontouched)
+ - [`[formState, input]`](#formstate-input)
+ - [Form State](#form-state)
+ - [Input Types](#input-types)
+- [License](#license)
+
+
+
+
## Motivation
Managing form state in React can be a bit unwieldy sometimes. There are [plenty of great solutions](https://www.npmjs.com/search?q=react%20forms&ranking=popularity) already available that make managing forms state a breeze. However, many of those solutions are opinionated, packed with tons of features that may end up not being used, and/or requires shipping a few extra bytes!
@@ -108,6 +133,27 @@ export default function RentCarForm() {
}
```
+### Global Handlers
+
+`useFormState` supports [a variety of form-level event handlers](#formoptions) that you could use to perform certain actions:
+
+```jsx
+export default function RentCarForm() {
+ const [formState, { email, password }] = useFormState(null, {
+ onChange(e, stateValues, nextStateValues) {
+ const { name, value } = e.target;
+ console.log(`the ${name} input has changed!`);
+ },
+ });
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
### Without Using a `` Element
`react-use-form-state` is not limited to actual forms. It can be used anywhere inputs are used.
@@ -131,17 +177,72 @@ function LoginForm({ onSubmit }) {
```js
import { useFormState } from 'react-use-form-state';
-function SignupForm({ onSubmit }) {
- const [formState, input] = useFormState(initialState);
+function FormComponent()
+ const [formState, input] = useFormState(initialState, formOptions);
// ...
}
```
-On initial render, `useFormState` takes an optional initial state object with keys as the name property of the form inputs, and values as the initial values of those inputs (similar to `defaultValue`/`defaultChecked`).
+### `initialState`
+
+`useFormState` takes an optional initial state object with keys as the name property of the form inputs, and values as the initial values of those inputs (similar to `defaultValue`/`defaultChecked`).
+
+### `formOptions`
+
+`useFormState` also accepts an optional form options object as a second argument with following properties:
+
+#### `formOptions.onBlur`
+
+A function that gets called upon any `blur` of the form's inputs. This functions provides access to the input's `blur` [`SyntheticEvent`](https://reactjs.org/docs/events.html)
+
+```js
+const [formState, inputs] = useFormState(null, {
+ onBlur(e) {
+ // accessing the inputs target that triggered the blur event
+ const { name, value, ...target } = e.target;
+ }
+});
+```
+
+#### `formOptions.onChange`
+
+A function that gets triggered upon any `change` of the form's inputs, and before updating `formState`.
+
+This function gives you access to the input's `change` [`SyntheticEvent`](https://reactjs.org/docs/events.html), the current `formState`, the next state after the change is applied.
+
+```js
+const [formState, inputs] = useFormState(null, {
+ onChange(e, stateValues, nextStateValues) {
+ // accessing the actual inputs target that triggered the change event
+ const { name, value, ...target } = e.target;
+ // the state values prior to applying the change
+ formState.values === stateValues; // true
+ // the state values after applying the change
+ nextStateValues;
+ // the state value of the input. See Input Types below for more information.
+ nextStateValues[name];
+ }
+});
+```
+
+#### `formOptions.onTouched`
+
+A function that gets called after an input inside the form has lost focus, and marked as touched. It will be called once throughout the component life cycle. This functions provides access to the input's `blur` [`SyntheticEvent`](https://reactjs.org/docs/events.html).
+
+```js
+const [formState, inputs] = useFormState(null, {
+ onTouched(e) {
+ // accessing the inputs target that triggered the blur event
+ const { name, value, ...target } = e.target;
+ }
+});
+```
+
+### `[formState, input]`
-It returns an array of two items, the first is the [form state](#form-state), and the second an [input types](#input-types) object.
+The return value of `useFormState`. An array of two items, the first is the [form state](#form-state), and the second an [input types](#input-types) object.
-### Form State
+#### Form State
The first item returned by `useFormState`.
@@ -171,7 +272,7 @@ formState = {
}
```
-### Input Types
+#### Input Types
The second item returned by `useFormState`.
diff --git a/src/index.d.ts b/src/index.d.ts
index ab67a5c..2ee4335 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,10 +1,13 @@
-// Type definitions for react-use-form-state 0.3
+// Type definitions for react-use-form-state 0.6.0
// Project: https://github.com/wsmd/react-use-form-state
// Definitions by: Waseem Dahman
export function useFormState<
T extends { [key: string]: string | string[] | number }
->(initialState?: T): [FormState, Inputs];
+>(
+ initialState?: T | null,
+ options?: Partial>,
+): [FormState, Inputs];
interface FormState {
values: InputValues;
@@ -12,6 +15,16 @@ interface FormState {
touched: InputValuesValidity;
}
+interface FormOptions {
+ onChange(
+ e: React.ChangeEvent,
+ stateValues: InputValues,
+ nextStateValues: InputValues,
+ ): void;
+ onBlur(e: React.FocusEvent): void;
+ onTouched(e: React.FocusEvent): void;
+}
+
interface Inputs {
selectMultiple(name: string): Omit & MultipleProp;
select(name: string): Omit;
@@ -19,6 +32,7 @@ interface Inputs {
color(name: string): InputProps;
password(name: string): InputProps;
text(name: string): InputProps;
+ textarea(name: string): Omit;
url(name: string): InputProps;
search(name: string): InputProps;
number(name: string): InputProps;
@@ -45,6 +59,8 @@ type Maybe = T | void;
type Omit = Pick>;
+type InputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
+
type InputValues = { readonly [A in keyof T]: T[A] } & {
readonly [key: string]: Maybe;
};
diff --git a/src/useFormState.js b/src/useFormState.js
index ec6de90..1df09a3 100644
--- a/src/useFormState.js
+++ b/src/useFormState.js
@@ -9,7 +9,17 @@ import {
SELECT_MULTIPLE,
} from './constants';
-export default function useFormState(initialState) {
+function noop() {}
+
+const defaultFromOptions = {
+ onChange: noop,
+ onBlur: noop,
+ onTouched: noop,
+};
+
+export default function useFormState(initialState, options) {
+ const formOptions = { ...defaultFromOptions, ...options };
+
const [state, setState] = useReducer(stateReducer, initialState || {});
const [touched, setTouchedState] = useReducer(stateReducer, {});
const [validity, setValidityState] = useReducer(stateReducer, {});
@@ -51,8 +61,7 @@ export default function useFormState(initialState) {
}
function getNextSelectMultipleValue(e) {
- const { options } = e.target;
- return Array.from(options).reduce(
+ return Array.from(e.target.options).reduce(
(values, option) =>
option.selected ? [...values, option.value] : values,
[],
@@ -108,9 +117,19 @@ export default function useFormState(initialState) {
if (isSelectMultiple) {
value = getNextSelectMultipleValue(e);
}
- setState({ [name]: value });
+
+ const partialNewState = { [name]: value };
+ const newState = { ...state, ...partialNewState };
+
+ formOptions.onChange(e, state, newState);
+
+ setState(partialNewState);
},
onBlur(e) {
+ if (!touched[name]) {
+ formOptions.onTouched(e);
+ }
+ formOptions.onBlur(e);
setTouchedState({ [name]: true });
setValidityState({ [name]: e.target.validity.valid });
},
diff --git a/test/test-utils.js b/test/test-utils.js
index f351669..ae97595 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -4,8 +4,8 @@ import { useFormState } from '../src';
const noop = () => {};
-const InputForm = ({ onChange, name, value, type }) => {
- const [formState, input] = useFormState();
+const InputForm = ({ onChange, name, value, type, formOptions = {} }) => {
+ const [formState, input] = useFormState(null, formOptions);
onChange(formState);
return ;
};
@@ -32,10 +32,16 @@ SelectForm.defaultProps = {
onChange: noop,
};
-export function renderInput(type, name, value) {
+export function renderInput(type, name, value, formOptions) {
const onChangeMock = jest.fn();
const { container } = render(
- ,
+ ,
);
const input = container.firstChild;
return {
diff --git a/test/useFormState.test.js b/test/useFormState.test.js
index af40057..47e13fc 100644
--- a/test/useFormState.test.js
+++ b/test/useFormState.test.js
@@ -42,6 +42,43 @@ describe('useFormState API', () => {
});
});
+describe('useFormState options', () => {
+ it('calls options.onChange when an input changes', () => {
+ const changeHandler = jest.fn();
+ const { change } = renderInput('text', 'username', undefined, {
+ onChange: changeHandler,
+ });
+ change({ value: 'w' });
+ expect(changeHandler).toHaveBeenCalledWith(
+ expect.any(Object), // SyntheticEvent
+ expect.objectContaining({ username: '' }),
+ expect.objectContaining({ username: 'w' }),
+ );
+ });
+
+ it('calls options.onBlur when an input changes', () => {
+ const blurHandler = jest.fn();
+ const { blur } = renderInput('text', 'username', undefined, {
+ onBlur: blurHandler,
+ });
+ blur();
+ expect(blurHandler).toHaveBeenCalledWith(expect.any(Object));
+ blur();
+ expect(blurHandler).toHaveBeenCalledTimes(2);
+ });
+
+ it('calls options.onTouched when an input changes', () => {
+ const touchedHandler = jest.fn();
+ const { blur } = renderInput('text', 'username', undefined, {
+ onTouched: touchedHandler,
+ });
+ blur();
+ expect(touchedHandler).toHaveBeenCalled();
+ blur();
+ expect(touchedHandler).toHaveBeenCalledTimes(1);
+ });
+});
+
describe('input type methods return correct props object', () => {
mockReactUseReducer();