Skip to content

Commit f3a7fff

Browse files
authored
Add new form and inputs control methods (#68)
* Add ability to update input value manually * Add ability to clear/reset all fields * update test * Add ability to clear/reset single inputs * refactor, add callbacks & fix broken tests * Add tests * reset error and validity state on setField * Update README.md * update code, tests, docs
1 parent de056e9 commit f3a7fff

7 files changed

+239
-61
lines changed

README.md

+120-40
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
</h1>
66

77
<p align="center">
8+
<a href="https://www.npmjs.com/package/react-use-form-state">
9+
<img src="https://img.shields.io/npm/v/react-use-form-state.svg" alt="Current Release" />
10+
</a>
11+
<a href="https://www.npmjs.com/package/react-use-form-state">
12+
<img src="https://badgen.net/npm/dt/react-use-form-state" alt="Downloads" />
13+
</a>
814
<a href="https://travis-ci.org/wsmd/react-use-form-state">
915
<img src="https://travis-ci.org/wsmd/react-use-form-state.svg?branch=master" alt="CI Build">
1016
</a>
@@ -14,12 +20,6 @@
1420
<a href="https://github.com/wsmd/react-use-form-state/blob/master/LICENSE">
1521
<img src="https://img.shields.io/github/license/wsmd/react-use-form-state.svg" alt="Licence">
1622
</a>
17-
<a href="https://www.npmjs.com/package/react-use-form-state">
18-
<img src="https://img.shields.io/npm/v/react-use-form-state.svg" alt="Current Release" />
19-
</a>
20-
<a href="https://www.npmjs.com/package/react-use-form-state">
21-
<img src="https://badgen.net/npm/dt/react-use-form-state" alt="Downloads" />
22-
</a>
2323
</p>
2424

2525
<details>
@@ -37,13 +37,16 @@
3737
- [Without Using a `<form />` Element](#without-using-a-form--element)
3838
- [Labels](#labels)
3939
- [Custom Controls](#custom-controls)
40+
- [Updating Fields Manually](#updating-fields-manually)
41+
- [Resetting The From State](#resetting-the-from-state)
4042
- [Working with TypeScript](#working-with-typescript)
4143
- [API](#api)
4244
- [`initialState`](#initialstate)
4345
- [`formOptions`](#formoptions)
4446
- [`formOptions.onBlur`](#formoptionsonblur)
4547
- [`formOptions.onChange`](#formoptionsonchange)
4648
- [`formOptions.onTouched`](#formoptionsontouched)
49+
- [`formOptions.onClear`](#formoptionsonclear)
4750
- [`formOptions.withIds`](#formoptionswithids)
4851
- [`[formState, inputs]`](#formstate-inputs)
4952
- [Form State](#form-state)
@@ -79,10 +82,15 @@ Please note that `react-use-form-state` requires `react@^16.8.0` as a peer depen
7982
```jsx
8083
import { useFormState } from 'react-use-form-state';
8184

82-
export default function SignUpForm() {
85+
export default function SignUpForm({ onSubmit }) {
8386
const [formState, { text, email, password, radio }] = useFormState();
87+
88+
function handleSubmit(e) {
89+
// ...
90+
}
91+
8492
return (
85-
<form onSubmit={() => console.log(formState)}>
93+
<form onSubmit={handleSubmit}>
8694
<input {...text('name')} />
8795
<input {...email('email')} required />
8896
<input {...password('password')} required minLength="8" />
@@ -93,31 +101,34 @@ export default function SignUpForm() {
93101
}
94102
```
95103

96-
From the example above, as the user fills in the form, `formState` will look something like this:
104+
From the example above, as the user fills in the form, the `formState` object will look something like this:
97105

98106
```js
99107
{
100-
"values": {
101-
"name": "Mary Poppins",
102-
"email": "[email protected]",
103-
"password": "1234",
104-
"plan": "free",
108+
values: {
109+
name: 'Mary Poppins',
110+
111+
password: '1234',
112+
plan: 'free',
105113
},
106-
"touched": {
107-
"name": true,
108-
"email": true,
109-
"password": true,
110-
"plan": true,
114+
touched: {
115+
name: true,
116+
email: true,
117+
password: true,
118+
plan: true,
111119
},
112-
"validity": {
113-
"name": true,
114-
"email": true,
115-
"password": false,
116-
"plan": true,
120+
validity: {
121+
name: true,
122+
email: true,
123+
password: false,
124+
plan: true,
117125
},
118-
"errors": {
119-
"password": "Please lengthen this text to 8 characters or more",
120-
}
126+
errors: {
127+
password: 'Please lengthen this text to 8 characters or more',
128+
},
129+
clear: Function,
130+
clearField: Function,
131+
setField: Function,
121132
}
122133
```
123134

@@ -350,6 +361,50 @@ function Widget() {
350361
}
351362
```
352363

364+
### Updating Fields Manually
365+
366+
There are cases where you may want to update the value of an input manually without user interaction. To do so, the `formState.setField` method can be used.
367+
368+
```js
369+
function Form() {
370+
const [formState, { text }] = useFormState();
371+
372+
function setNameField() {
373+
// manually setting the value of the "name" input
374+
formState.setField('name', 'Mary Poppins');
375+
}
376+
377+
return (
378+
<>
379+
<input {...text('name')} readOnly />
380+
<button onClick={setNameField}>Set Name</button>
381+
</>
382+
)
383+
}
384+
```
385+
386+
Please note that when `formState.setField` is called, any existing errors that might have been set due to previous interactions from the user will be cleared, and both of the `validity` and the `touched` states of the input will be set to `true`.
387+
388+
It's also possible to clear a single input's value using `formState.clearField`.
389+
390+
### Resetting The From State
391+
392+
All fields in the form can be cleared all at once at any time using `formState.clear`.
393+
394+
```js
395+
function Form() {
396+
const [formState, { text, email }] = useFormState();
397+
return (
398+
<>
399+
<input {...text("first_name")} />
400+
<input {...text("last_name")} />
401+
<input {...email("email")} />
402+
<button onClick={formState.clear}>Clear All Fields</button>
403+
</>
404+
);
405+
}
406+
```
407+
353408
## Working with TypeScript
354409

355410
When working with TypeScript, the compiler needs to know what values and inputs `useFormState` is expected to be working with.
@@ -401,7 +456,9 @@ import { useFormState } from 'react-use-form-state';
401456

402457
function FormComponent()
403458
const [formState, inputs] = useFormState(initialState, formOptions);
404-
// ...
459+
return (
460+
// ...
461+
)
405462
}
406463
```
407464

@@ -460,6 +517,20 @@ const [formState, inputs] = useFormState(null, {
460517
});
461518
```
462519

520+
#### `formOptions.onClear`
521+
522+
A function that gets called after calling `formState.clear` indicating that all fields in the form state are cleared successfully.
523+
524+
```js
525+
const [formState, inputs] = useFormState(null, {
526+
onClear() {
527+
// form state was cleared successfully
528+
}
529+
});
530+
531+
formState.clear(); // clearing the form state
532+
```
533+
463534
#### `formOptions.withIds`
464535

465536
Indicates whether `useFormState` should generate and pass an `id` attribute to its fields. This is helpful when [working with labels](#labels-and-ids).
@@ -497,29 +568,38 @@ The first item returned by `useFormState`.
497568
const [formState, inputs] = useFormState();
498569
```
499570

500-
An object describing the form state that updates during subsequent re-renders.
501-
502-
Form state consists of three nested objects:
503-
504-
- `values`: an object holding the state of each input being rendered.
505-
- `validity`: an object indicating whether the value of each input is valid.
506-
- `errors`: an object holding all errors resulting from input validations.
507-
- `touched`: an object indicating whether the input was touched (focused) by the user.
571+
An object containing the form state that updates during subsequent re-renders. It also include methods to update the form state manually.
508572

509573
```ts
510574
formState = {
575+
// an object holding the values of all input being rendered
511576
values: {
512-
[inputName: string]: string | string[] | boolean,
577+
[name: string]: string | string[] | boolean,
513578
},
579+
580+
// an object indicating whether the value of each input is valid
514581
validity: {
515-
[inputName: string]?: boolean,
582+
[name: string]?: boolean,
516583
},
584+
585+
// an object holding all errors resulting from input validations
517586
errors: {
518-
[input: name]?: any,
587+
[name: string]?: any,
519588
},
589+
590+
// an object indicating whether the input was touched (focused) by the user
520591
touched: {
521-
[inputName: string]?: boolean,
592+
[name: string]?: boolean,
522593
},
594+
595+
// clears all fields in the form
596+
clear(): void,
597+
598+
// clears the state of an input
599+
clearField(name: string): void,
600+
601+
// updates the value of an input
602+
setField(name: string, value: string): void,,
523603
}
524604
```
525605

src/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ interface FormState<T, E = StateErrors<T, string>> {
2323
validity: StateValidity<T>;
2424
touched: StateValidity<T>;
2525
errors: E;
26+
clear(): void;
27+
setField<K extends keyof T>(name: K, value: T[K]): void;
28+
clearField(name: keyof T): void;
2629
}
2730

2831
interface FormOptions<T> {
@@ -32,6 +35,7 @@ interface FormOptions<T> {
3235
nextStateValues: StateValues<T>,
3336
): void;
3437
onBlur(event: React.FocusEvent<InputElement>): void;
38+
onClear(): void;
3539
onTouched(event: React.FocusEvent<InputElement>): void;
3640
withIds: boolean | ((name: string, value?: string) => string);
3741
}

src/useFormState.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ const defaultFromOptions = {
2121
onChange: noop,
2222
onBlur: noop,
2323
onTouched: noop,
24+
onClear: noop,
2425
withIds: false,
2526
};
2627

2728
export default function useFormState(initialState, options) {
2829
const formOptions = { ...defaultFromOptions, ...options };
2930

30-
const formState = useState({ initialState });
31+
const formState = useState({ initialState, ...formOptions });
3132
const { getIdProp } = useInputId(formOptions.withIds);
3233
const { set: setDirty, has: isDirty } = useCache();
3334
const callbacks = useCache();
34-
3535
const devWarnings = useCache();
3636

3737
function warn(key, type, message) {
@@ -185,6 +185,12 @@ export default function useFormState(initialState, options) {
185185
if (!hasValueInState) {
186186
setInitialValue();
187187
}
188+
189+
// auto populating default values of touched
190+
if (formState.current.touched[name] == null) {
191+
formState.setTouched({ [name]: false });
192+
}
193+
188194
/**
189195
* Since checkbox and radio inputs have their own user-defined values,
190196
* and since checkbox inputs can be either an array or a boolean,
@@ -258,6 +264,7 @@ export default function useFormState(initialState, options) {
258264
* A) when it's either touched for the first time
259265
* B) when it's marked as dirty due to a value change
260266
*/
267+
/* istanbul ignore else */
261268
if (!formState.current.touched[name] || isDirty(name)) {
262269
validate(e);
263270
setDirty(name, false);
@@ -281,7 +288,10 @@ export default function useFormState(initialState, options) {
281288
);
282289

283290
return [
284-
formState.current,
291+
{
292+
...formState.current,
293+
...formState.controls,
294+
},
285295
{
286296
...inputPropsCreators,
287297
[LABEL]: (name, ownValue) => getIdProp('htmlFor', name, ownValue),

src/useState.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,27 @@ function stateReducer(state, newState) {
55
return isFunction(newState) ? newState(state) : { ...state, ...newState };
66
}
77

8-
export function useState({ initialState }) {
8+
export function useState({ initialState, onClear }) {
9+
const state = useRef();
910
const [values, setValues] = useReducer(stateReducer, initialState || {});
1011
const [touched, setTouched] = useReducer(stateReducer, {});
1112
const [validity, setValidity] = useReducer(stateReducer, {});
1213
const [errors, setError] = useReducer(stateReducer, {});
1314

14-
const state = useRef();
1515
state.current = { values, touched, validity, errors };
1616

17+
function setField(name, value, inputValidity, inputTouched, inputError) {
18+
setValues({ [name]: value });
19+
setTouched({ [name]: inputTouched });
20+
setValidity({ [name]: inputValidity });
21+
setError({ [name]: inputError });
22+
}
23+
24+
const clearField = name => setField(name);
25+
1726
return {
1827
/**
19-
* @type {{ values, touched, current, errors }}
28+
* @type {{ values, touched, validity, errors }}
2029
*/
2130
get current() {
2231
return state.current;
@@ -25,5 +34,15 @@ export function useState({ initialState }) {
2534
setTouched,
2635
setValidity,
2736
setError,
37+
controls: {
38+
clearField,
39+
clear() {
40+
Object.keys(state.current.values).forEach(clearField);
41+
onClear();
42+
},
43+
setField(name, value) {
44+
setField(name, value, true, true);
45+
},
46+
},
2847
};
2948
}

0 commit comments

Comments
 (0)