Skip to content

Commit 9d0d1dd

Browse files
committed
feat(forms): allow deep objects and arrays as form control names
1 parent 12a4ba2 commit 9d0d1dd

File tree

5 files changed

+87
-16
lines changed

5 files changed

+87
-16
lines changed

demo/FormExamples.jsx

+15
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,21 @@ export function FormExamples() {
220220
</div>
221221

222222
<FormGroupTextarea name="textareaField" label="Textarea field" />
223+
224+
{[0, 1].map((index) => (
225+
<div className="row" key={index}>
226+
<div className="col">
227+
<FormGroupInput name={`array[${index}].data.text`} label={`Array text ${index}`} />
228+
</div>
229+
<div className="col">
230+
<FormGroupSelect
231+
name={`array[${index}].data.select`}
232+
label={`Array select ${index}`}
233+
options={['Yes', 'No']}
234+
/>
235+
</div>
236+
</div>
237+
))}
223238
</Form>
224239
);
225240
}

src/forms/helpers/form-helpers.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
2+
import { getValueByPath } from '../../utils/getters-setters';
23

34
export const FormContext = React.createContext(null);
45

56
export function validateFormElement({ name, validations = [], formData, elementRefs }) {
67
let validationMessage = '';
7-
const value = formData[name];
8+
const value = getValueByPath(formData, name);
89

910
validations.some(({ message, validate }) => {
1011
const isValid = validate(value, formData);

src/forms/helpers/useForm.js

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState } from 'react';
2-
import { validateFormElement } from './form-helpers';
32
import { useArrayValueMap } from '../../utils/useValueMap';
3+
import { setValueByPath, deepClone, getValueByPath } from '../../utils/getters-setters';
4+
import { validateFormElement } from './form-helpers';
45

56
export function useForm(initialState, validations) {
67
const [formState, setFormState] = useState(initialState);
@@ -21,27 +22,17 @@ export function useForm(initialState, validations) {
2122
}
2223
},
2324
update(name, value) {
24-
setFormState((prevFormState) => {
25-
const nextState = {
26-
...prevFormState,
27-
[name]: value,
28-
};
29-
30-
return nextState;
31-
});
25+
setFormState((prevFormState) => nextState(prevFormState, name, value));
3226

3327
if (validations) {
34-
this.validateForm({
35-
...formState,
36-
[name]: value,
37-
});
28+
this.validateForm(nextState(formState, name, value));
3829
}
3930
},
4031
getFormData() {
4132
return formState;
4233
},
4334
getValue(name) {
44-
return formState[name];
35+
return getValueByPath(formState, name);
4536
},
4637
reset() {
4738
setFormState(initialState);
@@ -80,3 +71,7 @@ export function useForm(initialState, validations) {
8071
},
8172
};
8273
}
74+
75+
function nextState(previousState, path, value) {
76+
return setValueByPath(deepClone(previousState), path, value);
77+
}

src/utils/getters-setters.js

+38
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,41 @@ export function setValueByPath(obj, objPath, value) {
5757

5858
return obj || lastCursor;
5959
}
60+
61+
export function deepClone(item) {
62+
// null, undefined values check
63+
if (!item) {
64+
return item;
65+
}
66+
67+
if (Object.prototype.toString.call(item) === '[object Array]') {
68+
return item.map(deepClone);
69+
}
70+
71+
if (typeof item != 'object') {
72+
return item;
73+
}
74+
75+
// testing that this is DOM
76+
if (item.nodeType && typeof item.cloneNode == 'function') {
77+
return item.cloneNode(true);
78+
}
79+
80+
if (!item.prototype) {
81+
// check that this is a literal
82+
if (item instanceof Date) {
83+
return new Date(item);
84+
}
85+
86+
// it is an object literal
87+
const result = {};
88+
89+
for (const i in item) {
90+
result[i] = deepClone(item[i]);
91+
}
92+
93+
return result;
94+
}
95+
96+
return item;
97+
}

test/getters-setters.test.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setValueByPath, getValueByPath, splitPath } from '../src/utils/getters-setters';
1+
import { setValueByPath, getValueByPath, splitPath, deepClone } from '../src/utils/getters-setters';
22

33
test('splitPath should split path strings into hierarchical components', () => {
44
expect(splitPath('a.b.0.c')).toStrictEqual([
@@ -46,3 +46,25 @@ test('getValueByPath should get path value of complex objects', () => {
4646
expect(getValueByPath({ a: { '0': 6 } }, 'a[0]')).toBe(6);
4747
expect(getValueByPath({ a: [2, { b: [3, 4, 5, { c: { d: 'efg' } }] }] }, 'a[1].b.3.c')).toStrictEqual({ d: 'efg' });
4848
});
49+
50+
test('deepClone should not result in a shallow copy', () => {
51+
const source = {
52+
a: 'hello',
53+
c: 'test',
54+
po: 33,
55+
arr: [1, 2, 3, 4],
56+
anotherObj: {
57+
a: 33,
58+
str: 'whazzup',
59+
},
60+
};
61+
const dest = deepClone(source);
62+
63+
expect(dest).toStrictEqual(source);
64+
source.anotherObj.a = 200;
65+
expect(source.anotherObj.a).toBe(200);
66+
expect(dest.anotherObj.a).toBe(33);
67+
source.arr.push(5);
68+
expect(source.arr[4]).toBe(5);
69+
expect(dest.arr[4]).toBe(undefined);
70+
});

0 commit comments

Comments
 (0)