diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 6060cb68f..99ecaf2a4 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -135,11 +135,13 @@ export function deleteBy(obj: any, _path: any) { return doDelete(obj) } -const reFindNumbers0 = /^(\d*)$/gm -const reFindNumbers1 = /\.(\d*)\./gm -const reFindNumbers2 = /^(\d*)\./gm -const reFindNumbers3 = /\.(\d*$)/gm -const reFindMultiplePeriods = /\.{2,}/gm +const reLineOfOnlyDigits = /^(\d+)$/gm +// the second dot must be in a lookahead or the engine +// will skip subsequent numbers (like foo.0.1.) +const reDigitsBetweenDots = /\.(\d+)(?=\.)/gm +const reStartWithDigitThenDot = /^(\d+)\./gm +const reDotWithDigitsToEnd = /\.(\d+$)/gm +const reMultipleDots = /\.{2,}/gm const intPrefix = '__int__' const intReplace = `${intPrefix}$1` @@ -156,21 +158,25 @@ export function makePathArray(str: string | Array) { throw new Error('Path must be a string.') } - return str - .replace(/\[/g, '.') - .replace(/\]/g, '') - .replace(reFindNumbers0, intReplace) - .replace(reFindNumbers1, `.${intReplace}.`) - .replace(reFindNumbers2, `${intReplace}.`) - .replace(reFindNumbers3, `.${intReplace}`) - .replace(reFindMultiplePeriods, '.') - .split('.') - .map((d) => { - if (d.indexOf(intPrefix) === 0) { - return parseInt(d.substring(intPrefix.length), 10) - } - return d - }) + return ( + str + // Leading `[` may lead to wrong parsing down the line + // (Example: '[0][1]' should be '0.1', not '.0.1') + .replace(/(^\[)|]/gm, '') + .replace(/\[/g, '.') + .replace(reLineOfOnlyDigits, intReplace) + .replace(reDigitsBetweenDots, `.${intReplace}.`) + .replace(reStartWithDigitThenDot, `${intReplace}.`) + .replace(reDotWithDigitsToEnd, `.${intReplace}`) + .replace(reMultipleDots, '.') + .split('.') + .map((d) => { + if (d.indexOf(intPrefix) === 0) { + return parseInt(d.substring(intPrefix.length), 10) + } + return d + }) + ) } /** diff --git a/packages/form-core/tests/utils.spec.ts b/packages/form-core/tests/utils.spec.ts index 18a493ae0..27464cac9 100644 --- a/packages/form-core/tests/utils.spec.ts +++ b/packages/form-core/tests/utils.spec.ts @@ -96,6 +96,43 @@ describe('setBy', () => { kids: [...structure.kids, { name: 'John' }], }) }) + + it('should preserve arrays when setting them within other arrays', () => { + const table: { field: { value: number }[][] } = { + field: [ + [ + { + value: 0, + }, + { + value: 1, + }, + ], + [ + { + value: 2, + }, + ], + ], + } + + const newTable = setBy(table, 'field[0][1].value', 2) + expect(newTable.field).toStrictEqual([ + [ + { + value: 0, + }, + { + value: 2, + }, + ], + [ + { + value: 2, + }, + ], + ]) + }) }) describe('deleteBy', () => { @@ -138,27 +175,54 @@ describe('deleteBy', () => { }) describe('makePathArray', () => { - it('should convert dot notation to array', () => { - expect(makePathArray('name')).toEqual(['name']) - expect(makePathArray('mother.name')).toEqual(['mother', 'name']) - expect(makePathArray('kids[0].name')).toEqual(['kids', 0, 'name']) - expect(makePathArray('kids[0].name[1]')).toEqual(['kids', 0, 'name', 1]) - expect(makePathArray('kids[0].name[1].age')).toEqual([ - 'kids', - 0, - 'name', - 1, - 'age', - ]) - expect(makePathArray('kids[0].name[1].age[2]')).toEqual([ - 'kids', + it('should convert chained property access', () => { + expect(makePathArray('a')).toEqual(['a']) + expect(makePathArray('a.b')).toEqual(['a', 'b']) + expect(makePathArray('foo.bar.baz')).toEqual(['foo', 'bar', 'baz']) + }) + + it('should convert property access followed by array indeces', () => { + expect(makePathArray('a[0]')).toEqual(['a', 0]) + expect(makePathArray('foo[1]')).toEqual(['foo', 1]) + expect(makePathArray('a.b[2]')).toEqual(['a', 'b', 2]) + }) + + it('should convert chained array indeces', () => { + expect(makePathArray('a[0][1]')).toEqual(['a', 0, 1]) + expect(makePathArray('foo[3][4][5]')).toEqual(['foo', 3, 4, 5]) + }) + + it('should convert array indeces followed by property access', () => { + expect(makePathArray('a[0].b')).toEqual(['a', 0, 'b']) + expect(makePathArray('foo[1].bar')).toEqual(['foo', 1, 'bar']) + expect(makePathArray('[2].bar')).toEqual([2, 'bar']) + expect(makePathArray('[1][5].baz')).toEqual([1, 5, 'baz']) + }) + + it('should convert mixed chains of access', () => { + expect(makePathArray('a.b[0].c[1].d')).toEqual(['a', 'b', 0, 'c', 1, 'd']) + expect(makePathArray('x[0].y[1].z')).toEqual(['x', 0, 'y', 1, 'z']) + }) + + it('should handle deeply nested paths', () => { + expect(makePathArray('a.b[0][1].c.d[2][3].e')).toEqual([ + 'a', + 'b', 0, - 'name', 1, - 'age', + 'c', + 'd', 2, + 3, + 'e', ]) }) + + it('should convert paths starting with multiple array indeces', () => { + expect(makePathArray('[0][1]')).toEqual([0, 1]) + expect(makePathArray('[2][3].a')).toEqual([2, 3, 'a']) + expect(makePathArray('[4][5][6].b[7]')).toEqual([4, 5, 6, 'b', 7]) + }) }) describe('determineFormLevelErrorSourceAndValue', () => {