diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index c231b2dee..305f36f7e 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -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' @@ -553,8 +559,9 @@ export class FormApi { deleteField = >(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 }) } diff --git a/packages/form-core/src/tests/FormApi.spec.ts b/packages/form-core/src/tests/FormApi.spec.ts index fa4031e98..55e64a40c 100644 --- a/packages/form-core/src/tests/FormApi.spec.ts +++ b/packages/form-core/src/tests/FormApi.spec.ts @@ -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[] + } + + const form = new FormApi() + + 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[] + } + + const form = new FormApi() + + 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: { @@ -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') diff --git a/packages/form-core/src/tests/utils.spec.ts b/packages/form-core/src/tests/utils.spec.ts new file mode 100644 index 000000000..ed807a5c1 --- /dev/null +++ b/packages/form-core/src/tests/utils.spec.ts @@ -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() + }) +}) diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index e7cd31a20..9f2204a9d 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -17,8 +17,7 @@ export function functionalUpdate( * 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] @@ -52,22 +51,59 @@ export function setBy(obj: any, _path: any, updater: Updater) { } } + 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