Skip to content

Commit 8e4f9b8

Browse files
authored
fix(form-core): Fix setBy/getBy not handling arrays properly (#1517)
fix: fix makePathArray not working properly with arrays
1 parent b01ee7f commit 8e4f9b8

File tree

2 files changed

+106
-36
lines changed

2 files changed

+106
-36
lines changed

packages/form-core/src/utils.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,13 @@ export function deleteBy(obj: any, _path: any) {
135135
return doDelete(obj)
136136
}
137137

138-
const reFindNumbers0 = /^(\d*)$/gm
139-
const reFindNumbers1 = /\.(\d*)\./gm
140-
const reFindNumbers2 = /^(\d*)\./gm
141-
const reFindNumbers3 = /\.(\d*$)/gm
142-
const reFindMultiplePeriods = /\.{2,}/gm
138+
const reLineOfOnlyDigits = /^(\d+)$/gm
139+
// the second dot must be in a lookahead or the engine
140+
// will skip subsequent numbers (like foo.0.1.)
141+
const reDigitsBetweenDots = /\.(\d+)(?=\.)/gm
142+
const reStartWithDigitThenDot = /^(\d+)\./gm
143+
const reDotWithDigitsToEnd = /\.(\d+$)/gm
144+
const reMultipleDots = /\.{2,}/gm
143145

144146
const intPrefix = '__int__'
145147
const intReplace = `${intPrefix}$1`
@@ -156,21 +158,25 @@ export function makePathArray(str: string | Array<string | number>) {
156158
throw new Error('Path must be a string.')
157159
}
158160

159-
return str
160-
.replace(/\[/g, '.')
161-
.replace(/\]/g, '')
162-
.replace(reFindNumbers0, intReplace)
163-
.replace(reFindNumbers1, `.${intReplace}.`)
164-
.replace(reFindNumbers2, `${intReplace}.`)
165-
.replace(reFindNumbers3, `.${intReplace}`)
166-
.replace(reFindMultiplePeriods, '.')
167-
.split('.')
168-
.map((d) => {
169-
if (d.indexOf(intPrefix) === 0) {
170-
return parseInt(d.substring(intPrefix.length), 10)
171-
}
172-
return d
173-
})
161+
return (
162+
str
163+
// Leading `[` may lead to wrong parsing down the line
164+
// (Example: '[0][1]' should be '0.1', not '.0.1')
165+
.replace(/(^\[)|]/gm, '')
166+
.replace(/\[/g, '.')
167+
.replace(reLineOfOnlyDigits, intReplace)
168+
.replace(reDigitsBetweenDots, `.${intReplace}.`)
169+
.replace(reStartWithDigitThenDot, `${intReplace}.`)
170+
.replace(reDotWithDigitsToEnd, `.${intReplace}`)
171+
.replace(reMultipleDots, '.')
172+
.split('.')
173+
.map((d) => {
174+
if (d.indexOf(intPrefix) === 0) {
175+
return parseInt(d.substring(intPrefix.length), 10)
176+
}
177+
return d
178+
})
179+
)
174180
}
175181

176182
/**

packages/form-core/tests/utils.spec.ts

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,43 @@ describe('setBy', () => {
9696
kids: [...structure.kids, { name: 'John' }],
9797
})
9898
})
99+
100+
it('should preserve arrays when setting them within other arrays', () => {
101+
const table: { field: { value: number }[][] } = {
102+
field: [
103+
[
104+
{
105+
value: 0,
106+
},
107+
{
108+
value: 1,
109+
},
110+
],
111+
[
112+
{
113+
value: 2,
114+
},
115+
],
116+
],
117+
}
118+
119+
const newTable = setBy(table, 'field[0][1].value', 2)
120+
expect(newTable.field).toStrictEqual([
121+
[
122+
{
123+
value: 0,
124+
},
125+
{
126+
value: 2,
127+
},
128+
],
129+
[
130+
{
131+
value: 2,
132+
},
133+
],
134+
])
135+
})
99136
})
100137

101138
describe('deleteBy', () => {
@@ -138,27 +175,54 @@ describe('deleteBy', () => {
138175
})
139176

140177
describe('makePathArray', () => {
141-
it('should convert dot notation to array', () => {
142-
expect(makePathArray('name')).toEqual(['name'])
143-
expect(makePathArray('mother.name')).toEqual(['mother', 'name'])
144-
expect(makePathArray('kids[0].name')).toEqual(['kids', 0, 'name'])
145-
expect(makePathArray('kids[0].name[1]')).toEqual(['kids', 0, 'name', 1])
146-
expect(makePathArray('kids[0].name[1].age')).toEqual([
147-
'kids',
148-
0,
149-
'name',
150-
1,
151-
'age',
152-
])
153-
expect(makePathArray('kids[0].name[1].age[2]')).toEqual([
154-
'kids',
178+
it('should convert chained property access', () => {
179+
expect(makePathArray('a')).toEqual(['a'])
180+
expect(makePathArray('a.b')).toEqual(['a', 'b'])
181+
expect(makePathArray('foo.bar.baz')).toEqual(['foo', 'bar', 'baz'])
182+
})
183+
184+
it('should convert property access followed by array indeces', () => {
185+
expect(makePathArray('a[0]')).toEqual(['a', 0])
186+
expect(makePathArray('foo[1]')).toEqual(['foo', 1])
187+
expect(makePathArray('a.b[2]')).toEqual(['a', 'b', 2])
188+
})
189+
190+
it('should convert chained array indeces', () => {
191+
expect(makePathArray('a[0][1]')).toEqual(['a', 0, 1])
192+
expect(makePathArray('foo[3][4][5]')).toEqual(['foo', 3, 4, 5])
193+
})
194+
195+
it('should convert array indeces followed by property access', () => {
196+
expect(makePathArray('a[0].b')).toEqual(['a', 0, 'b'])
197+
expect(makePathArray('foo[1].bar')).toEqual(['foo', 1, 'bar'])
198+
expect(makePathArray('[2].bar')).toEqual([2, 'bar'])
199+
expect(makePathArray('[1][5].baz')).toEqual([1, 5, 'baz'])
200+
})
201+
202+
it('should convert mixed chains of access', () => {
203+
expect(makePathArray('a.b[0].c[1].d')).toEqual(['a', 'b', 0, 'c', 1, 'd'])
204+
expect(makePathArray('x[0].y[1].z')).toEqual(['x', 0, 'y', 1, 'z'])
205+
})
206+
207+
it('should handle deeply nested paths', () => {
208+
expect(makePathArray('a.b[0][1].c.d[2][3].e')).toEqual([
209+
'a',
210+
'b',
155211
0,
156-
'name',
157212
1,
158-
'age',
213+
'c',
214+
'd',
159215
2,
216+
3,
217+
'e',
160218
])
161219
})
220+
221+
it('should convert paths starting with multiple array indeces', () => {
222+
expect(makePathArray('[0][1]')).toEqual([0, 1])
223+
expect(makePathArray('[2][3].a')).toEqual([2, 3, 'a'])
224+
expect(makePathArray('[4][5][6].b[7]')).toEqual([4, 5, 6, 'b', 7])
225+
})
162226
})
163227

164228
describe('determineFormLevelErrorSourceAndValue', () => {

0 commit comments

Comments
 (0)