Skip to content

Commit

Permalink
fix: deleteField inside of an array should now work
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
Christian24 and crutchcorn authored Dec 3, 2023
1 parent 6c9e652 commit 4e8bf2a
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 9 deletions.
11 changes: 9 additions & 2 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Store } from '@tanstack/store'
import type { DeepKeys, DeepValue, Updater } from './utils'
import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
import {
deleteBy,
functionalUpdate,
getBy,
isNonEmptyArray,
setBy,
} from './utils'
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
import type { ValidationError, Validator } from './types'

Expand Down Expand Up @@ -553,8 +559,9 @@ export class FormApi<TFormData, ValidatorType> {
deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
this.store.setState((prev) => {
const newState = { ...prev }
delete newState.values[field as keyof TFormData]
newState.values = deleteBy(newState.values, field)
delete newState.fieldMeta[field]

return newState
})
}
Expand Down
58 changes: 57 additions & 1 deletion packages/form-core/src/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,63 @@ describe('form api', () => {
expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])
})

it('should handle fields inside an array', async () => {
interface Employee {
firstName: string
}
interface Form {
employees: Partial<Employee>[]
}

const form = new FormApi<Form, unknown>()

const field = new FieldApi({
form,
name: 'employees',
defaultValue: [],
})

field.mount()

const fieldInArray = new FieldApi({
form,
name: `employees.${0}.firstName`,
defaultValue: 'Darcy',
})
fieldInArray.mount()
expect(field.state.value.length).toBe(1)
expect(fieldInArray.getValue()).toBe('Darcy')
})

it('should handle deleting fields in an array', async () => {
interface Employee {
firstName: string
}
interface Form {
employees: Partial<Employee>[]
}

const form = new FormApi<Form, unknown>()

const field = new FieldApi({
form,
name: 'employees',
defaultValue: [],
})

field.mount()

const fieldInArray = new FieldApi({
form,
name: `employees.${0}.firstName`,
defaultValue: 'Darcy',
})
fieldInArray.mount()
form.deleteField(`employees.${0}.firstName`)
expect(field.state.value.length).toBe(1)
expect(Object.keys(field.state.value[0]!).length).toBe(0)
})

it('should not wipe values when updating', () => {
const form = new FormApi({
defaultValues: {
Expand Down Expand Up @@ -500,7 +557,6 @@ describe('form api', () => {

form.mount()
field.mount()

expect(form.state.errors.length).toBe(0)
field.setValue('other', { touch: true })
field.validate('blur')
Expand Down
73 changes: 73 additions & 0 deletions packages/form-core/src/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'
import { deleteBy, getBy, setBy } from '../utils'

describe('getBy', () => {
const structure = {
name: 'Marc',
kids: [
{ name: 'Stephen', age: 10 },
{ name: 'Taylor', age: 15 },
],
mother: {
name: 'Lisa',
},
}

it('should get subfields by path', () => {
expect(getBy(structure, 'name')).toBe(structure.name)
expect(getBy(structure, 'mother.name')).toBe(structure.mother.name)
})

it('should get array subfields by path', () => {
expect(getBy(structure, 'kids.0.name')).toBe(structure.kids[0]!.name)
expect(getBy(structure, 'kids.0.age')).toBe(structure.kids[0]!.age)
})
})

describe('setBy', () => {
const structure = {
name: 'Marc',
kids: [
{ name: 'Stephen', age: 10 },
{ name: 'Taylor', age: 15 },
],
mother: {
name: 'Lisa',
},
}

it('should set subfields by path', () => {
expect(setBy(structure, 'name', 'Lisa').name).toBe('Lisa')
expect(setBy(structure, 'mother.name', 'Tina').mother.name).toBe('Tina')
})

it('should set array subfields by path', () => {
expect(setBy(structure, 'kids.0.name', 'Taylor').kids[0].name).toBe(
'Taylor',
)
expect(setBy(structure, 'kids.0.age', 20).kids[0].age).toBe(20)
})
})

describe('deleteBy', () => {
const structure = {
name: 'Marc',
kids: [
{ name: 'Stephen', age: 10 },
{ name: 'Taylor', age: 15 },
],
mother: {
name: 'Lisa',
},
}

it('should delete subfields by path', () => {
expect(deleteBy(structure, 'name').name).not.toBeDefined()
expect(deleteBy(structure, 'mother.name').mother.name).not.toBeDefined()
})

it('should delete array subfields by path', () => {
expect(deleteBy(structure, 'kids.0.name').kids[0].name).not.toBeDefined()
expect(deleteBy(structure, 'kids.0.age').kids[0].age).not.toBeDefined()
})
})
48 changes: 42 additions & 6 deletions packages/form-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export function functionalUpdate<TInput, TOutput = TInput>(
* Get a value from an object using a path, including dot notation.
*/
export function getBy(obj: any, path: any) {
const pathArray = makePathArray(path)
const pathObj = pathArray
const pathObj = makePathArray(path)
return pathObj.reduce((current: any, pathPart: any) => {
if (typeof current !== 'undefined') {
return current[pathPart]
Expand Down Expand Up @@ -52,22 +51,59 @@ export function setBy(obj: any, _path: any, updater: Updater<any>) {
}
}

if (Array.isArray(parent) && key !== undefined) {
const prefix = parent.slice(0, key)
return [
...(prefix.length ? prefix : new Array(key)),
doSet(parent[key]),
...parent.slice(key + 1),
]
}
return [...new Array(key), doSet()]
}

return doSet(obj)
}

/**
* Delete a field on an object using a path, including dot notation.
*/
export function deleteBy(obj: any, _path: any) {
const path = makePathArray(_path)

function doDelete(parent: any): any {
if (path.length === 1) {
const finalPath = path[0]!
const { [finalPath]: remove, ...rest } = parent
return rest
}

const key = path.shift()

if (typeof key === 'string') {
if (typeof parent === 'object') {
return {
...parent,
[key]: doDelete(parent[key]),
}
}
}

if (typeof key === 'number') {
if (Array.isArray(parent)) {
const prefix = parent.slice(0, key)
return [
...(prefix.length ? prefix : new Array(key)),
doSet(parent[key]),
doDelete(parent[key]),
...parent.slice(key + 1),
]
}
return [...new Array(key), doSet()]
}

throw new Error('Uh oh!')
throw new Error('It seems we have created an infinite loop in deleteBy. ')
}

return doSet(obj)
return doDelete(obj)
}

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

0 comments on commit 4e8bf2a

Please sign in to comment.