Skip to content

Commit 4e8bf2a

Browse files
fix: deleteField inside of an array should now work
* Add basic tests for arrays * Ran prettier * Ran prettier * Add new test for bug * Fix bug regarding preserved values even if field is umounted * Run prettier * Update store subscription when removingFields * Fix delete field * Fix delete field * Fix bug with deleteField inside an array * Fix bug with deleteField inside an array * Fix bug with deleteField inside an array * Improve error * Improve error * Add tests for utils * Add tests for utils * chore: fix test errors, remove errant test utils --------- Co-authored-by: Corbin Crutchley <[email protected]>
1 parent 6c9e652 commit 4e8bf2a

File tree

4 files changed

+181
-9
lines changed

4 files changed

+181
-9
lines changed

packages/form-core/src/FormApi.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Store } from '@tanstack/store'
22
import type { DeepKeys, DeepValue, Updater } from './utils'
3-
import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
3+
import {
4+
deleteBy,
5+
functionalUpdate,
6+
getBy,
7+
isNonEmptyArray,
8+
setBy,
9+
} from './utils'
410
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
511
import type { ValidationError, Validator } from './types'
612

@@ -553,8 +559,9 @@ export class FormApi<TFormData, ValidatorType> {
553559
deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
554560
this.store.setState((prev) => {
555561
const newState = { ...prev }
556-
delete newState.values[field as keyof TFormData]
562+
newState.values = deleteBy(newState.values, field)
557563
delete newState.fieldMeta[field]
564+
558565
return newState
559566
})
560567
}

packages/form-core/src/tests/FormApi.spec.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,63 @@ describe('form api', () => {
226226
expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])
227227
})
228228

229+
it('should handle fields inside an array', async () => {
230+
interface Employee {
231+
firstName: string
232+
}
233+
interface Form {
234+
employees: Partial<Employee>[]
235+
}
236+
237+
const form = new FormApi<Form, unknown>()
238+
239+
const field = new FieldApi({
240+
form,
241+
name: 'employees',
242+
defaultValue: [],
243+
})
244+
245+
field.mount()
246+
247+
const fieldInArray = new FieldApi({
248+
form,
249+
name: `employees.${0}.firstName`,
250+
defaultValue: 'Darcy',
251+
})
252+
fieldInArray.mount()
253+
expect(field.state.value.length).toBe(1)
254+
expect(fieldInArray.getValue()).toBe('Darcy')
255+
})
256+
257+
it('should handle deleting fields in an array', async () => {
258+
interface Employee {
259+
firstName: string
260+
}
261+
interface Form {
262+
employees: Partial<Employee>[]
263+
}
264+
265+
const form = new FormApi<Form, unknown>()
266+
267+
const field = new FieldApi({
268+
form,
269+
name: 'employees',
270+
defaultValue: [],
271+
})
272+
273+
field.mount()
274+
275+
const fieldInArray = new FieldApi({
276+
form,
277+
name: `employees.${0}.firstName`,
278+
defaultValue: 'Darcy',
279+
})
280+
fieldInArray.mount()
281+
form.deleteField(`employees.${0}.firstName`)
282+
expect(field.state.value.length).toBe(1)
283+
expect(Object.keys(field.state.value[0]!).length).toBe(0)
284+
})
285+
229286
it('should not wipe values when updating', () => {
230287
const form = new FormApi({
231288
defaultValues: {
@@ -500,7 +557,6 @@ describe('form api', () => {
500557

501558
form.mount()
502559
field.mount()
503-
504560
expect(form.state.errors.length).toBe(0)
505561
field.setValue('other', { touch: true })
506562
field.validate('blur')
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { deleteBy, getBy, setBy } from '../utils'
3+
4+
describe('getBy', () => {
5+
const structure = {
6+
name: 'Marc',
7+
kids: [
8+
{ name: 'Stephen', age: 10 },
9+
{ name: 'Taylor', age: 15 },
10+
],
11+
mother: {
12+
name: 'Lisa',
13+
},
14+
}
15+
16+
it('should get subfields by path', () => {
17+
expect(getBy(structure, 'name')).toBe(structure.name)
18+
expect(getBy(structure, 'mother.name')).toBe(structure.mother.name)
19+
})
20+
21+
it('should get array subfields by path', () => {
22+
expect(getBy(structure, 'kids.0.name')).toBe(structure.kids[0]!.name)
23+
expect(getBy(structure, 'kids.0.age')).toBe(structure.kids[0]!.age)
24+
})
25+
})
26+
27+
describe('setBy', () => {
28+
const structure = {
29+
name: 'Marc',
30+
kids: [
31+
{ name: 'Stephen', age: 10 },
32+
{ name: 'Taylor', age: 15 },
33+
],
34+
mother: {
35+
name: 'Lisa',
36+
},
37+
}
38+
39+
it('should set subfields by path', () => {
40+
expect(setBy(structure, 'name', 'Lisa').name).toBe('Lisa')
41+
expect(setBy(structure, 'mother.name', 'Tina').mother.name).toBe('Tina')
42+
})
43+
44+
it('should set array subfields by path', () => {
45+
expect(setBy(structure, 'kids.0.name', 'Taylor').kids[0].name).toBe(
46+
'Taylor',
47+
)
48+
expect(setBy(structure, 'kids.0.age', 20).kids[0].age).toBe(20)
49+
})
50+
})
51+
52+
describe('deleteBy', () => {
53+
const structure = {
54+
name: 'Marc',
55+
kids: [
56+
{ name: 'Stephen', age: 10 },
57+
{ name: 'Taylor', age: 15 },
58+
],
59+
mother: {
60+
name: 'Lisa',
61+
},
62+
}
63+
64+
it('should delete subfields by path', () => {
65+
expect(deleteBy(structure, 'name').name).not.toBeDefined()
66+
expect(deleteBy(structure, 'mother.name').mother.name).not.toBeDefined()
67+
})
68+
69+
it('should delete array subfields by path', () => {
70+
expect(deleteBy(structure, 'kids.0.name').kids[0].name).not.toBeDefined()
71+
expect(deleteBy(structure, 'kids.0.age').kids[0].age).not.toBeDefined()
72+
})
73+
})

packages/form-core/src/utils.ts

+42-6
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export function functionalUpdate<TInput, TOutput = TInput>(
1717
* Get a value from an object using a path, including dot notation.
1818
*/
1919
export function getBy(obj: any, path: any) {
20-
const pathArray = makePathArray(path)
21-
const pathObj = pathArray
20+
const pathObj = makePathArray(path)
2221
return pathObj.reduce((current: any, pathPart: any) => {
2322
if (typeof current !== 'undefined') {
2423
return current[pathPart]
@@ -52,22 +51,59 @@ export function setBy(obj: any, _path: any, updater: Updater<any>) {
5251
}
5352
}
5453

54+
if (Array.isArray(parent) && key !== undefined) {
55+
const prefix = parent.slice(0, key)
56+
return [
57+
...(prefix.length ? prefix : new Array(key)),
58+
doSet(parent[key]),
59+
...parent.slice(key + 1),
60+
]
61+
}
62+
return [...new Array(key), doSet()]
63+
}
64+
65+
return doSet(obj)
66+
}
67+
68+
/**
69+
* Delete a field on an object using a path, including dot notation.
70+
*/
71+
export function deleteBy(obj: any, _path: any) {
72+
const path = makePathArray(_path)
73+
74+
function doDelete(parent: any): any {
75+
if (path.length === 1) {
76+
const finalPath = path[0]!
77+
const { [finalPath]: remove, ...rest } = parent
78+
return rest
79+
}
80+
81+
const key = path.shift()
82+
83+
if (typeof key === 'string') {
84+
if (typeof parent === 'object') {
85+
return {
86+
...parent,
87+
[key]: doDelete(parent[key]),
88+
}
89+
}
90+
}
91+
5592
if (typeof key === 'number') {
5693
if (Array.isArray(parent)) {
5794
const prefix = parent.slice(0, key)
5895
return [
5996
...(prefix.length ? prefix : new Array(key)),
60-
doSet(parent[key]),
97+
doDelete(parent[key]),
6198
...parent.slice(key + 1),
6299
]
63100
}
64-
return [...new Array(key), doSet()]
65101
}
66102

67-
throw new Error('Uh oh!')
103+
throw new Error('It seems we have created an infinite loop in deleteBy. ')
68104
}
69105

70-
return doSet(obj)
106+
return doDelete(obj)
71107
}
72108

73109
const reFindNumbers0 = /^(\d*)$/gm

0 commit comments

Comments
 (0)