Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix deleteField inside of an array #510

Merged
merged 25 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
28a9a4f
Add basic tests for arrays
Christian24 Nov 1, 2023
37decf8
Ran prettier
Christian24 Nov 1, 2023
008f36f
Ran prettier
Christian24 Nov 1, 2023
cc92627
Add new test for bug
Christian24 Nov 2, 2023
a8f28c4
Fix bug regarding preserved values even if field is umounted
Christian24 Nov 2, 2023
864d872
Run prettier
Christian24 Nov 3, 2023
dda899f
Update store subscription when removingFields
Christian24 Nov 4, 2023
ff6e307
Merge branch 'main' into main
Christian24 Nov 4, 2023
e4bd9ce
Fix delete field
Christian24 Nov 5, 2023
d55f8c8
Merge remote-tracking branch 'origin/main'
Christian24 Nov 5, 2023
281dc0f
Merge branch 'main' into main
Christian24 Nov 5, 2023
0e5dc38
Fix delete field
Christian24 Nov 5, 2023
232740c
Merge remote-tracking branch 'origin/main'
Christian24 Nov 5, 2023
0cc7b36
Fix bug with deleteField inside an array
Christian24 Nov 5, 2023
924eaeb
Merge branch 'main' into main
Christian24 Nov 5, 2023
6ea8802
Fix bug with deleteField inside an array
Christian24 Nov 5, 2023
455fcbe
Merge branch 'main' of https://github.com/Christian24/form
Christian24 Nov 5, 2023
1aaa74c
Fix bug with deleteField inside an array
Christian24 Nov 5, 2023
d444343
Improve error
Christian24 Nov 5, 2023
3d1d0cf
Improve error
Christian24 Nov 5, 2023
2835cc3
Add tests for utils
Christian24 Nov 5, 2023
5b3f280
Add tests for utils
Christian24 Nov 5, 2023
d88e8b7
Merge branch 'main' into Christian24/main
crutchcorn Dec 3, 2023
20f3769
Merge branch 'utils' into Christian24/main
crutchcorn Dec 3, 2023
b41b2ef
chore: fix test errors, remove errant test utils
crutchcorn Dec 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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