Skip to content

Commit 72d981f

Browse files
authored
Allow non-string values to be used as ownValue (#26)
* allow non-string values to be used as ownValue * always stringify input value of checkbox and radio * add test * cleanup * update README.md * remove eslint-disable
1 parent 7c50e73 commit 72d981f

9 files changed

+83
-35
lines changed

.eslintrc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"consistent-return": "off",
88
"getter-return": "off",
99
"arrow-parens": ["error", "as-needed"],
10-
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }]
10+
"react/jsx-filename-extension": [
11+
"error",
12+
{ "extensions": [".js", ".jsx"] }
13+
],
14+
"import/prefer-default-export": false
1115
}
1216
}

README.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -284,27 +284,27 @@ An object with keys as input types. Each type is a function that returns the app
284284

285285
The following types are currently supported:
286286

287-
| Type and Usage | State Shape |
288-
| ----------------------------------------------------------- | ----------------------------------- |
289-
| `<input {...input.email(name: string) />` | `{ [name: string]: string }` |
290-
| `<input {...input.color(name: string) />` | `{ [name: string]: string }` |
291-
| `<input {...input.password(name: string) />` | `{ [name: string]: string }` |
292-
| `<input {...input.text(name: string) />` | `{ [name: string]: string }` |
293-
| `<input {...input.url(name: string) />` | `{ [name: string]: string }` |
294-
| `<input {...input.search(name: string) />` | `{ [name: string]: string }` |
295-
| `<input {...input.number(name: string) />` | `{ [name: string]: string }` |
296-
| `<input {...input.range(name: string) />` | `{ [name: string]: string }` |
297-
| `<input {...input.tel(name: string) />` | `{ [name: string]: string }` |
298-
| `<input {...input.radio(name: string, value: string) />` | `{ [name: string]: string }` |
299-
| `<input {...input.checkbox(name: string, value: string) />` | `{ [name: string]: Array<string> }` |
300-
| `<input {...input.checkbox(name: string) />` | `{ [name: string]: boolean }` |
301-
| `<input {...input.date(name: string) />` | `{ [name: string]: string }` |
302-
| `<input {...input.month(name: string) />` | `{ [name: string]: string }` |
303-
| `<input {...input.week(name: string) />` | `{ [name: string]: string }` |
304-
| `<input {...input.time(name: string) />` | `{ [name: string]: string }` |
305-
| `<select {...input.select(name: string) />` | `{ [name: string]: string }` |
306-
| `<select {...input.selectMultiple(name: string) />` | `{ [name: string]: Array<string> }` |
307-
| `<textarea {...input.textarea(name: string) />` | `{ [name: string]: string }` |
287+
| Type and Usage | State Shape |
288+
| -------------------------------------------------------------- | ----------------------------------- |
289+
| `<input {...input.email(name: string) />` | `{ [name: string]: string }` |
290+
| `<input {...input.color(name: string) />` | `{ [name: string]: string }` |
291+
| `<input {...input.password(name: string) />` | `{ [name: string]: string }` |
292+
| `<input {...input.text(name: string) />` | `{ [name: string]: string }` |
293+
| `<input {...input.url(name: string) />` | `{ [name: string]: string }` |
294+
| `<input {...input.search(name: string) />` | `{ [name: string]: string }` |
295+
| `<input {...input.number(name: string) />` | `{ [name: string]: string }` |
296+
| `<input {...input.range(name: string) />` | `{ [name: string]: string }` |
297+
| `<input {...input.tel(name: string) />` | `{ [name: string]: string }` |
298+
| `<input {...input.radio(name: string, ownValue: string) />` | `{ [name: string]: string }` |
299+
| `<input {...input.checkbox(name: string, ownValue: string) />` | `{ [name: string]: Array<string> }` |
300+
| `<input {...input.checkbox(name: string) />` | `{ [name: string]: boolean }` |
301+
| `<input {...input.date(name: string) />` | `{ [name: string]: string }` |
302+
| `<input {...input.month(name: string) />` | `{ [name: string]: string }` |
303+
| `<input {...input.week(name: string) />` | `{ [name: string]: string }` |
304+
| `<input {...input.time(name: string) />` | `{ [name: string]: string }` |
305+
| `<select {...input.select(name: string) />` | `{ [name: string]: string }` |
306+
| `<select {...input.selectMultiple(name: string) />` | `{ [name: string]: Array<string> }` |
307+
| `<textarea {...input.textarea(name: string) />` | `{ [name: string]: string }` |
308308

309309

310310
## License

src/index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ interface Inputs {
5656
number(name: string): BaseInputProps;
5757
range(name: string): BaseInputProps;
5858
tel(name: string): BaseInputProps;
59-
radio(name: string, value: string): RadioProps;
59+
radio(name: string, ownValue: OwnValue): RadioProps;
6060
/**
6161
* Checkbox inputs with a value will be treated as a collection of choices.
6262
* Their values in in the form state will be of type Array<string>
6363
*/
64-
checkbox(name: string, value: string): CheckboxProps;
64+
checkbox(name: string, ownValue: OwnValue): CheckboxProps;
6565
/**
6666
* Checkbox inputs without a value will be treated as toggles. Their values in
6767
* in the form state will be of type boolean
@@ -75,6 +75,8 @@ interface Inputs {
7575

7676
type InputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
7777

78+
type OwnValue = string | number | boolean | string[];
79+
7880
interface BaseInputProps {
7981
onChange(e: any): void;
8082
onBlur(e: any): void;

src/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
/* eslint import/prefer-default-export: off */
2-
31
export { default as useFormState } from './useFormState';

src/stateReducer.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
export default function stateReducer(state, newState) {
1+
/**
2+
* Shallowly merge newState into state
3+
*/
4+
export function stateReducer(state, newState) {
25
return { ...state, ...newState };
36
}

src/toString.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Cast non-string values to a string, with the exception of functions, symbols,
3+
* and undefined.
4+
*/
5+
export function toString(value) {
6+
switch (typeof value) {
7+
case 'function':
8+
case 'symbol':
9+
case 'undefined':
10+
return '';
11+
default:
12+
return '' + value; // eslint-disable-line prefer-template
13+
}
14+
}

src/useFormState.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useReducer } from 'react';
2-
import stateReducer from './stateReducer';
2+
import { stateReducer } from './stateReducer';
3+
import { toString } from './toString';
34
import {
45
TYPES,
56
SELECT,
@@ -25,7 +26,7 @@ export default function useFormState(initialState, options) {
2526
const [validity, setValidityState] = useReducer(stateReducer, {});
2627

2728
const createPropsGetter = type => (name, ownValue) => {
28-
const hasOwnValue = !!ownValue;
29+
const hasOwnValue = !!toString(ownValue);
2930
const hasValueInState = state[name] !== undefined;
3031
const isCheckbox = type === CHECKBOX;
3132
const isRadio = type === RADIO;
@@ -79,7 +80,7 @@ export default function useFormState(initialState, options) {
7980
},
8081
get checked() {
8182
if (isRadio) {
82-
return state[name] === ownValue;
83+
return state[name] === toString(ownValue);
8384
}
8485
if (isCheckbox) {
8586
if (!hasOwnValue) {
@@ -91,7 +92,9 @@ export default function useFormState(initialState, options) {
9192
* <input {...input.checkbox('option1')} />
9293
* <input {...input.checkbox('option1', 'value_of_option1')} />
9394
*/
94-
return hasValueInState ? state[name].includes(ownValue) : false;
95+
return hasValueInState
96+
? state[name].includes(toString(ownValue))
97+
: false;
9598
}
9699
},
97100
get value() {
@@ -105,7 +108,7 @@ export default function useFormState(initialState, options) {
105108
* returning the value of input from the current form state is illogical
106109
*/
107110
if (isCheckbox || isRadio) {
108-
return ownValue;
111+
return toString(ownValue);
109112
}
110113
return hasValueInState ? state[name] : '';
111114
},

test/types.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,16 @@ formState.touched.colors;
5151
formState.validity.username;
5252

5353
<input {...input.checkbox('name', 'value')} />;
54+
<input {...input.radio('name', 'value')} />;
55+
<input {...input.radio('name', true)} />;
56+
<input {...input.radio('name', 123)} />;
57+
<input {...input.radio('name', ['a', 'b'])} />;
5458
<input {...input.color('name')} />;
5559
<input {...input.date('name')} />;
5660
<input {...input.email('name')} />;
5761
<input {...input.month('name')} />;
5862
<input {...input.number('name')} />;
5963
<input {...input.password('name')} />;
60-
<input {...input.radio('name', 'value')} />;
6164
<input {...input.range('name')} />;
6265
<input {...input.search('name')} />;
6366
<input {...input.tel('name')} />;

test/useFormState.test.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ describe('input type methods return correct props object', () => {
125125
checked: false,
126126
onChange: expect.any(Function),
127127
onBlur: expect.any(Function),
128+
value: '',
128129
});
129130
});
130131

@@ -143,6 +144,26 @@ describe('input type methods return correct props object', () => {
143144
});
144145
});
145146

147+
/**
148+
* Stringify non-string ownValue of checkbox and radio
149+
*/
150+
it.each`
151+
type | ownValue | expected
152+
${'array'} | ${[1, 2]} | ${'1,2'}
153+
${'boolean'} | ${false} | ${'false'}
154+
${'number'} | ${1} | ${'1'}
155+
${'object'} | ${{}} | ${'[object Object]'}
156+
${'function'} | ${() => {}} | ${''}
157+
${'Symbol'} | ${Symbol('')} | ${''}
158+
`(
159+
'stringify ownValue of type $type for checkbox and radio',
160+
({ ownValue, expected }) => {
161+
const [, input] = useFormState();
162+
expect(input.checkbox('option1', ownValue).value).toEqual(expected);
163+
expect(input.radio('option2', ownValue).value).toEqual(expected);
164+
},
165+
);
166+
146167
/**
147168
* Select doesn't need a type
148169
*/
@@ -221,9 +242,9 @@ describe('inputs receive default values from initial state', () => {
221242
const initialState = { option1: true };
222243
const [, input] = useFormState(initialState);
223244
expect(input.checkbox('option1').checked).toEqual(true);
224-
expect(input.checkbox('option1').value).toEqual(undefined);
245+
expect(input.checkbox('option1').value).toEqual('');
225246
expect(input.checkbox('option2').checked).toEqual(false);
226-
expect(input.checkbox('option2').value).toEqual(undefined);
247+
expect(input.checkbox('option2').value).toEqual('');
227248
});
228249

229250
it('sets initial "checked" for type "radio"', () => {

0 commit comments

Comments
 (0)