Skip to content

Commit ff3b94c

Browse files
authored
feat(forms2): experimental new approach to form state (#38)
1 parent 244121d commit ff3b94c

13 files changed

+456
-75
lines changed

.eslintrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"ecmaFeatures": {
2020
"jsx": true
2121
},
22-
"ecmaVersion": 2018,
22+
"ecmaVersion": 2020,
2323
"sourceType": "module"
2424
},
2525
"plugins": ["react", "react-hooks", "import"],

demo/Form2Examples.jsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import { Form2, FormInput2, FormSelect2, FormSwitch2 } from '../dist/main';
4+
5+
export function Form2Examples() {
6+
return (
7+
<div>
8+
Alternative Form implementation
9+
<Form2
10+
initialValues={{ attrA: 'ABC' }}
11+
onSubmit={console.info.bind(console, 'onSubmit')}
12+
onChange={console.info.bind(console, 'onChange')}
13+
transform={(formData) => ({
14+
__v: formData.__v ? formData.__v + 1 : 1,
15+
attrB: `${formData.attrB || ''}A`,
16+
})}
17+
>
18+
<div className="form-group">
19+
<label htmlFor="">AttrA</label>
20+
<FormInput2 name="attrA" />
21+
</div>
22+
<div className="form-group">
23+
<label htmlFor="">AttrB</label>
24+
<FormInput2 name="attrB" />
25+
</div>
26+
<div className="form-group">
27+
<label htmlFor="">AttrC</label>
28+
<FormSelect2 name="attrC" options={[1, 2, 3]} />
29+
</div>
30+
<div className="form-group">
31+
<label htmlFor="">AttrD</label>
32+
<FormSwitch2 name="attrD" id="attrD" />
33+
</div>
34+
<button className="btn btn-success">Submit</button>
35+
</Form2>
36+
</div>
37+
);
38+
}

demo/demo.jsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import { ListGroupExamples } from './ListGroupExamples';
1414
import { ToastsExamples } from './ToastsExamples';
1515
import { DropdownExamples } from './DropdownExamples';
1616
import { TreeViewExamples } from './TreeViewExamples';
17+
import { Form2Examples } from './Form2Examples';
1718

1819
ReactDOM.render(
1920
<div className="mt-3">
2021
<React.StrictMode>
2122
<StatefulTabs
2223
vertical={true}
2324
onlyRenderActiveTab={true}
24-
initialTab={8}
25+
initialTab={3}
2526
tabs={[
2627
{
2728
title: 'Dialog',
@@ -35,6 +36,10 @@ ReactDOM.render(
3536
title: 'Forms',
3637
content: <FormExamples />,
3738
},
39+
{
40+
title: 'Forms2',
41+
content: <Form2Examples />,
42+
},
3843
{
3944
title: 'List groups',
4045
content: <ListGroupExamples />,

src/forms/helpers/form-helpers.js

+71-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
2-
import { isFunction, isUndefined, isArray, isObject } from 'js-var-type';
2+
import { isFunction, isUndefined, isArray, isObject, isEmptyStringLike, isBoolean } from 'js-var-type';
33
import { getValueByPath } from '../../utils/getters-setters';
4+
import { fromDatetimeLocal, toDatetimeLocal } from '../../utils/formatters';
45

56
export const FormContext = React.createContext(null);
67

@@ -29,6 +30,34 @@ export function validateFormElement({ name, validations = [], formData, elementR
2930
return validationMessage;
3031
}
3132

33+
export function getTargetValue(target) {
34+
let value = target.type === 'checkbox' ? target.checked : target.value;
35+
36+
if (target.type === 'number') {
37+
value = target.valueAsNumber;
38+
if (isNaN(value)) {
39+
value = undefined;
40+
}
41+
}
42+
43+
if (target.type === 'datetime-local') {
44+
value = fromDatetimeLocal(target.value);
45+
}
46+
47+
if (target.type === 'select-one') {
48+
if (value && ['{', '['].includes(value[0])) {
49+
try {
50+
value = JSON.parse(value);
51+
} catch (error) {
52+
// eslint-disable-next-line no-console
53+
console.error(error);
54+
}
55+
}
56+
}
57+
58+
return value;
59+
}
60+
3261
export function handleInputChange(formState, event) {
3362
const target = event.target;
3463
const value = target.type === 'checkbox' ? target.checked : target.value;
@@ -85,3 +114,44 @@ export function getSelectedOption(value, options, trackBy) {
85114
export function getOptionsType(options) {
86115
return options.length > 0 ? typeof options[0].value : undefined;
87116
}
117+
118+
function getEmptyValue(type) {
119+
switch (type) {
120+
case 'boolean':
121+
return false;
122+
123+
case 'array':
124+
return [];
125+
126+
default:
127+
return '';
128+
}
129+
}
130+
131+
export function encode(value, type) {
132+
if (isEmptyStringLike(value)) {
133+
return getEmptyValue(type);
134+
}
135+
136+
if (type === 'datetime-local') {
137+
return toDatetimeLocal(value);
138+
}
139+
140+
if (type === 'number' && isNaN(value)) {
141+
return;
142+
}
143+
144+
return value;
145+
}
146+
147+
export function decode(value, type) {
148+
if (type === 'number') {
149+
return parseFloat(value);
150+
}
151+
152+
if (type === 'boolean') {
153+
return isBoolean(value) ? value : value === 'true';
154+
}
155+
156+
return value;
157+
}

src/forms/helpers/useFormControl.js

+2-72
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useContext, useCallback } from 'react';
2-
import { isEmptyStringLike, isBoolean, isFunction } from 'js-var-type';
3-
import { FormContext } from './form-helpers';
4-
import { toDatetimeLocal, fromDatetimeLocal } from '../../utils/formatters';
2+
import { isFunction } from 'js-var-type';
3+
import { FormContext, getTargetValue, decode, encode } from './form-helpers';
54

65
export function useFormControl(name, type) {
76
const formState = useContext(FormContext);
@@ -51,72 +50,3 @@ export function useFormControl(name, type) {
5150
getFormSubmitedAttempted: () => formState.getSubmitedAttempted(),
5251
};
5352
}
54-
55-
function getEmptyValue(type) {
56-
switch (type) {
57-
case 'boolean':
58-
return false;
59-
60-
case 'array':
61-
return [];
62-
63-
default:
64-
return '';
65-
}
66-
}
67-
68-
function encode(value, type) {
69-
if (isEmptyStringLike(value)) {
70-
return getEmptyValue(type);
71-
}
72-
73-
if (type === 'datetime-local') {
74-
return toDatetimeLocal(value);
75-
}
76-
77-
if (type === 'number' && isNaN(value)) {
78-
return;
79-
}
80-
81-
return value;
82-
}
83-
84-
function decode(value, type) {
85-
if (type === 'number') {
86-
return parseFloat(value);
87-
}
88-
89-
if (type === 'boolean') {
90-
return isBoolean(value) ? value : value === 'true';
91-
}
92-
93-
return value;
94-
}
95-
96-
function getTargetValue(target) {
97-
let value = target.type === 'checkbox' ? target.checked : target.value;
98-
99-
if (target.type === 'number') {
100-
value = target.valueAsNumber;
101-
if (isNaN(value)) {
102-
value = undefined;
103-
}
104-
}
105-
106-
if (target.type === 'datetime-local') {
107-
value = fromDatetimeLocal(target.value);
108-
}
109-
110-
if (target.type === 'select-one') {
111-
if (value && ['{', '['].includes(value[0])) {
112-
try {
113-
value = JSON.parse(value);
114-
} catch (error) {
115-
// eslint-disable-next-line no-console
116-
console.error(error);
117-
}
118-
}
119-
}
120-
121-
return value;
122-
}

src/forms2/Form.jsx

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { useCallback } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { useFormHelper, FormContext } from './helpers/useFormHelper';
5+
6+
export function Form2({ children, initialValues, onSubmit, onChange, transform, debounceWait }) {
7+
const formHelper = useFormHelper(initialValues, { debounceWait, transform, onChange });
8+
9+
const handleSubmit = useCallback(
10+
(e) => {
11+
e.preventDefault();
12+
e.stopPropagation();
13+
14+
onSubmit(formHelper.getFormData());
15+
},
16+
[formHelper, onSubmit]
17+
);
18+
19+
return (
20+
<form onSubmit={handleSubmit}>
21+
<FormContext.Provider value={formHelper}>{children}</FormContext.Provider>
22+
</form>
23+
);
24+
}
25+
Form2.defaultProps = {
26+
debounceWait: 500,
27+
onChange: () => {},
28+
};
29+
Form2.propTypes = {
30+
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
31+
initialValues: PropTypes.object,
32+
onChange: PropTypes.func,
33+
onSubmit: PropTypes.func.isRequired,
34+
transform: PropTypes.func,
35+
debounceWait: PropTypes.number,
36+
};

src/forms2/FormInput.jsx

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { useFormControl2 } from './helpers/useFormControl';
5+
import { booleanOrFunction } from '../forms/helpers/form-helpers';
6+
7+
export function FormInput2({ type, name, required: _required, disabled: _disabled, afterChange, ..._attrs }) {
8+
const { getValue, handleOnChangeFactory, getFormData } = useFormControl2(name);
9+
10+
const disabled = booleanOrFunction(_disabled, getFormData());
11+
const required = booleanOrFunction(_required, getFormData());
12+
13+
const attrs = {
14+
..._attrs,
15+
disabled,
16+
name,
17+
required,
18+
type,
19+
};
20+
21+
if (type === 'datetime-local') {
22+
attrs.defaultValue = getValue();
23+
} else {
24+
attrs.value = getValue();
25+
}
26+
27+
return <input {...attrs} className="form-control" onChange={handleOnChangeFactory(afterChange)} />;
28+
}
29+
30+
FormInput2.defaultProps = {
31+
type: 'text',
32+
};
33+
34+
FormInput2.propTypes = {
35+
afterChange: PropTypes.func,
36+
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
37+
id: PropTypes.string,
38+
max: PropTypes.string,
39+
maxLength: PropTypes.string,
40+
min: PropTypes.string,
41+
minLength: PropTypes.string,
42+
name: PropTypes.string.isRequired,
43+
pattern: PropTypes.string,
44+
placeholder: PropTypes.string,
45+
readOnly: PropTypes.bool,
46+
required: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
47+
step: PropTypes.string,
48+
type: PropTypes.string,
49+
};

0 commit comments

Comments
 (0)