Skip to content

Commit 6f28c77

Browse files
authored
Treat checkbox inputs with no values as toggles (#7)
* Treat checkbox inputs with no values as toggles * Ignore dist from test watcher * Add checkbox overload * Refactor code * Update README.md
1 parent 3fe5912 commit 6f28c77

File tree

5 files changed

+111
-33
lines changed

5 files changed

+111
-33
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ The following types are currently supported:
197197
| `<input {...input.tel(name: string) />` | `{ [name: string]: string }` |
198198
| `<input {...input.radio(name: string, value: string) />` | `{ [name: string]: string }` |
199199
| `<input {...input.checkbox(name: string, value: string) />` | `{ [name: string]: Array<string> }` |
200+
| `<input {...input.checkbox(name: string) />` | `{ [name: string]: boolean }` |
200201
| `<input {...input.date(name: string) />` | `{ [name: string]: string }` |
201202
| `<input {...input.month(name: string) />` | `{ [name: string]: string }` |
202203
| `<input {...input.week(name: string) />` | `{ [name: string]: string }` |

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"dist"
3333
],
3434
"jest": {
35+
"watchPathIgnorePatterns": [
36+
"dist"
37+
],
3538
"collectCoverageFrom": [
3639
"src/**"
3740
],

src/index.d.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Type definitions for react-use-form-state 0.2
1+
// Type definitions for react-use-form-state 0.3
22
// Project: https://github.com/wsmd/react-use-form-state
33
// Definitions by: Waseem Dahman <https://github.com/wsmd>
44

@@ -24,7 +24,16 @@ interface Inputs {
2424
range(name: string): InputProps;
2525
tel(name: string): InputProps;
2626
radio(name: string, value: string): InputProps & CheckedProp;
27+
/**
28+
* Checkbox inputs with a value will be treated as a collection of choices.
29+
* Their values in in the form state will be of type Array<string>
30+
*/
2731
checkbox(name: string, value: string): InputProps & CheckedProp;
32+
/**
33+
* Checkbox inputs without a value will be treated as toggles. Their values in
34+
* in the form state will be of type boolean
35+
*/
36+
checkbox(name: string): InputProps & CheckedProp;
2837
date(name: string): InputProps;
2938
month(name: string): InputProps;
3039
week(name: string): InputProps;

src/useFormState.js

+65-32
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,99 @@ import stateReducer from './stateReducer';
33
import { TYPES, SELECT, CHECKBOX, RADIO } from './constants';
44

55
export default function useFormState(initialState) {
6-
const [values, setFormState] = useReducer(stateReducer, initialState || {});
6+
const [state, setState] = useReducer(stateReducer, initialState || {});
77
const [touched, setTouchedState] = useReducer(stateReducer, {});
88
const [validity, setValidityState] = useReducer(stateReducer, {});
99

10-
const createPropsGetter = type => (name, value) => {
11-
const hasValue = values[name] !== undefined;
10+
const createPropsGetter = type => (name, ownValue) => {
11+
const hasOwnValue = !!ownValue;
12+
const hasValueInState = state[name] !== undefined;
13+
const isCheckbox = type === CHECKBOX;
14+
const isRadio = type === RADIO;
15+
16+
function setInitialValue() {
17+
let value = '';
18+
if (isCheckbox) {
19+
/**
20+
* If a checkbox has a user-defined value, its value the form state
21+
* value will be an array. Otherwise it will be considered a toggle.
22+
*/
23+
value = hasOwnValue ? [] : false;
24+
}
25+
setState({ [name]: value });
26+
}
27+
28+
function getNextCheckboxValue(e) {
29+
const { value, checked } = e.target;
30+
if (!hasOwnValue) {
31+
return checked;
32+
}
33+
const checkedValues = new Set(state[name]);
34+
if (checked) {
35+
checkedValues.add(value);
36+
} else {
37+
checkedValues.delete(value);
38+
}
39+
return Array.from(checkedValues);
40+
}
41+
1242
const inputProps = {
1343
name,
1444
get type() {
15-
if (type !== SELECT) {
16-
return type;
17-
}
45+
if (type !== SELECT) return type;
1846
},
1947
get checked() {
20-
if (type === CHECKBOX) {
21-
return hasValue ? values[name].includes(value) : false;
48+
if (isRadio) {
49+
return state[name] === ownValue;
2250
}
23-
if (type === RADIO) {
24-
return values[name] === value;
51+
if (isCheckbox) {
52+
if (!hasOwnValue) {
53+
return state[name] || false;
54+
}
55+
/**
56+
* @todo Handle the case where two checkbox inputs share the same
57+
* name, but one has a value, the other doesn't (throws currently).
58+
* <input {...input.checkbox('option1')} />
59+
* <input {...input.checkbox('option1', 'value_of_option1')} />
60+
*/
61+
return hasValueInState ? state[name].includes(ownValue) : false;
2562
}
2663
},
2764
get value() {
28-
// populating values of the form state on first render
29-
if (!hasValue) {
30-
setFormState({ [name]: type === CHECKBOX ? [] : '' });
31-
}
32-
if (type === CHECKBOX || type === RADIO) {
33-
return value;
65+
// auto populating initial state values on first render
66+
if (!hasValueInState) {
67+
setInitialValue();
3468
}
35-
if (hasValue) {
36-
return values[name];
69+
/**
70+
* Since checkbox and radio inputs have their own user-defined values,
71+
* and since checkbox inputs can be either an array or a boolean,
72+
* returning the value of input from the current form state is illogical
73+
*/
74+
if (isCheckbox || isRadio) {
75+
return ownValue;
3776
}
38-
return '';
77+
return hasValueInState ? state[name] : '';
3978
},
4079
onChange(e) {
41-
const { value: targetValue, checked } = e.target;
42-
let inputValue = targetValue;
43-
if (type === CHECKBOX) {
44-
const checkedValues = new Set(values[name]);
45-
if (checked) {
46-
checkedValues.add(inputValue);
47-
} else {
48-
checkedValues.delete(inputValue);
49-
}
50-
inputValue = Array.from(checkedValues);
80+
let { value } = e.target;
81+
if (isCheckbox) {
82+
value = getNextCheckboxValue(e);
5183
}
52-
setFormState({ [name]: inputValue });
84+
setState({ [name]: value });
5385
},
5486
onBlur(e) {
5587
setTouchedState({ [name]: true });
5688
setValidityState({ [name]: e.target.validity.valid });
5789
},
5890
};
91+
5992
return inputProps;
6093
};
6194

62-
const typeMethods = TYPES.reduce(
95+
const inputPropsCreators = TYPES.reduce(
6396
(methods, type) => ({ ...methods, [type]: createPropsGetter(type) }),
6497
{},
6598
);
6699

67-
return [{ values, touched, validity }, typeMethods];
100+
return [{ values: state, validity, touched }, inputPropsCreators];
68101
}

test/userFormState.test.js

+32
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ describe('type methods return correct props object', () => {
108108
});
109109
});
110110

111+
/**
112+
* Checkbox must have a type, value, and checked
113+
*/
114+
it('returns props for type "checkbox" without a value', () => {
115+
const [, input] = useFormState();
116+
expect(input.checkbox('option')).toEqual({
117+
type: 'checkbox',
118+
name: 'option',
119+
checked: expect.any(Boolean),
120+
onChange: expect.any(Function),
121+
onBlur: expect.any(Function),
122+
});
123+
});
124+
111125
/**
112126
* Radio must have a type, value, and checked
113127
*/
@@ -170,6 +184,15 @@ describe('inputs receive default values from initial state', () => {
170184
expect(input.checkbox('options', 'option_3').checked).toEqual(false);
171185
});
172186

187+
it('sets initiate "checked" for type "checkbox" without a value', () => {
188+
const initialState = { option1: true };
189+
const [, input] = useFormState(initialState);
190+
expect(input.checkbox('option1').checked).toEqual(true);
191+
expect(input.checkbox('option1').value).toEqual(undefined);
192+
expect(input.checkbox('option2').checked).toEqual(false);
193+
expect(input.checkbox('option2').value).toEqual(undefined);
194+
});
195+
173196
it('sets initiate "checked" for type "radio"', () => {
174197
const [, input] = useFormState({ option: 'no' });
175198
expect(input.radio('option', 'yes').checked).toEqual(false);
@@ -216,6 +239,15 @@ describe('onChange updates inputs value', () => {
216239
expect(state[name]).toEqual([]);
217240
});
218241

242+
it('updates value for type "checkbox" without a value', () => {
243+
const [, input] = useFormState();
244+
const name = 'checkbox-input';
245+
input.checkbox(name).onChange({ target: { checked: true } });
246+
expect(state[name]).toEqual(true);
247+
input.checkbox(name).onChange({ target: { checked: false } });
248+
expect(state[name]).toEqual(false);
249+
});
250+
219251
it('updates value for type "radio"', () => {
220252
const [, input] = useFormState();
221253
const name = 'radio-input';

0 commit comments

Comments
 (0)