Skip to content

Commit 935fb43

Browse files
committed
Fix removeBatch mutator removing more fields than expected.
1 parent f69ab19 commit 935fb43

File tree

2 files changed

+145
-70
lines changed

2 files changed

+145
-70
lines changed

src/removeBatch.js

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,97 @@
11
// @flow
22
import type { MutableState, Mutator, Tools } from 'final-form'
3-
import moveFieldState from './moveFieldState'
3+
import copyField from './copyField'
44
import { escapeRegexTokens } from './utils'
55

6-
const countBelow = (array, value) =>
7-
array.reduce((count, item) => (item < value ? count + 1 : count), 0)
6+
const binarySearch = (list: number[], value: number): number => {
7+
// This algorithm assumes the items in list is unique
8+
let first = 0
9+
let last = list.length - 1
10+
let middle = 0
11+
12+
while (first <= last) {
13+
middle = Math.floor((first + last) / 2)
14+
if (list[middle] === value) {
15+
return middle
16+
}
17+
18+
if (list[middle] > value) {
19+
last = middle - 1
20+
} else {
21+
first = middle + 1
22+
}
23+
}
24+
25+
return ~first
26+
}
827

928
const removeBatch: Mutator<any> = (
1029
[name, indexes]: any[],
1130
state: MutableState<any>,
1231
{ changeValue }: Tools<any>
1332
) => {
33+
if (indexes.length === 0) {
34+
return []
35+
}
36+
1437
const sortedIndexes: number[] = [...indexes]
1538
sortedIndexes.sort()
16-
// remove duplicates
17-
for (let i = 0; i < sortedIndexes.length; i++) {
18-
if (i > 0 && sortedIndexes[i] === sortedIndexes[i - 1]) {
19-
sortedIndexes.splice(i--, 1)
39+
40+
// Remove duplicates
41+
for (let i = sortedIndexes.length - 1; i > 0; i -= 1) {
42+
if (sortedIndexes[i] === sortedIndexes[i - 1]) {
43+
sortedIndexes.splice(i, 1)
2044
}
2145
}
2246

2347
let returnValue = []
2448
changeValue(state, name, (array: ?(any[])): ?(any[]) => {
2549
// use original order of indexes for return value
2650
returnValue = indexes.map(index => array && array[index])
27-
if (!array || !sortedIndexes.length) {
51+
52+
if (!array) {
2853
return array
2954
}
3055

3156
const copy = [...array]
32-
const removed = []
33-
sortedIndexes.forEach((index: number) => {
34-
copy.splice(index - removed.length, 1)
35-
removed.push(array && array[index])
36-
})
57+
for (let i = sortedIndexes.length - 1; i >= 0; i -= 1) {
58+
const index = sortedIndexes[i]
59+
copy.splice(index, 1)
60+
}
61+
3762
return copy
3863
})
3964

4065
// now we have to remove any subfields for our indexes,
4166
// and decrement all higher indexes.
4267
const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`)
43-
const newState = { ...state, fields: {} }
68+
const newFields = {}
4469
Object.keys(state.fields).forEach(key => {
4570
const tokens = pattern.exec(key)
4671
if (tokens) {
4772
const fieldIndex = Number(tokens[1])
48-
if (!~sortedIndexes.indexOf(fieldIndex)) {
49-
// not one of the removed indexes
50-
// shift all higher ones down
51-
const decrementedKey = `${name}[${fieldIndex -
52-
countBelow(sortedIndexes, fieldIndex)}]${tokens[2]}`
53-
moveFieldState(newState, state.fields[key], decrementedKey, state)
73+
const indexOfFieldIndex = binarySearch(sortedIndexes, fieldIndex)
74+
if (indexOfFieldIndex >= 0) {
75+
// One of the removed indices
76+
return
77+
}
78+
79+
if (fieldIndex > sortedIndexes[0]) {
80+
// Shift all higher indices down
81+
const decrementedKey = `${name}[${fieldIndex - ~indexOfFieldIndex}]${
82+
tokens[2]
83+
}`
84+
copyField(state.fields, key, newFields, decrementedKey)
85+
return
5486
}
55-
} else {
56-
newState.fields[key] = state.fields[key]
5787
}
88+
89+
// Keep this field that does not match the name,
90+
// or has index smaller than what is being removed
91+
newFields[key] = state.fields[key]
5892
})
59-
state.fields = newState.fields
93+
94+
state.fields = newFields
6095
return returnValue
6196
}
6297

src/removeBatch.test.js

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,51 @@ describe('removeBatch', () => {
187187
expect(result).toBeUndefined()
188188
})
189189

190-
it('should return the original array if no indexes are specified to be removed', () => {
191-
const op = getOp([])
192-
const result = op(['a', 'b', 'c', 'd', 'e'])
193-
expect(Array.isArray(result)).toBe(true)
194-
expect(result).toEqual(['a', 'b', 'c', 'd', 'e'])
190+
it('should keep the original state if no indexes are specified to be removed', () => {
191+
const array = ['a', 'b', 'c', 'd', 'e']
192+
function blur0() {}
193+
function change0() {}
194+
function focus0() {}
195+
const state = {
196+
formState: {
197+
values: {
198+
foo: array
199+
}
200+
},
201+
fields: {
202+
'foo[0]': {
203+
name: 'foo[0]',
204+
blur: blur0,
205+
change: change0,
206+
focus: focus0,
207+
touched: true,
208+
error: 'A Error'
209+
}
210+
}
211+
}
212+
const changeValue = jest.fn()
213+
const returnValue = removeBatch(['foo[0]', []], state, {
214+
changeValue
215+
})
216+
expect(returnValue).toEqual([])
217+
expect(state.formState.values.foo).toBe(array) // no change
218+
expect(state).toEqual({
219+
formState: {
220+
values: {
221+
foo: array
222+
}
223+
},
224+
fields: {
225+
'foo[0]': {
226+
name: 'foo[0]',
227+
blur: blur0,
228+
change: change0,
229+
focus: focus0,
230+
touched: true,
231+
error: 'A Error'
232+
}
233+
}
234+
})
195235
})
196236

197237
it('should remove the values at the specified indexes', () => {
@@ -239,6 +279,14 @@ describe('removeBatch', () => {
239279
}
240280
},
241281
fields: {
282+
'foo[4]': {
283+
name: 'foo[4]',
284+
blur: blur4,
285+
change: change4,
286+
focus: focus4,
287+
touched: true,
288+
error: 'E Error'
289+
},
242290
'foo[0]': {
243291
name: 'foo[0]',
244292
blur: blur0,
@@ -271,31 +319,32 @@ describe('removeBatch', () => {
271319
touched: false,
272320
error: 'D Error'
273321
},
274-
'foo[4]': {
275-
name: 'foo[4]',
276-
blur: blur4,
277-
change: change4,
278-
focus: focus4,
279-
touched: true,
280-
error: 'E Error'
281-
},
282322
anotherField: {
283323
name: 'anotherField',
284324
touched: false
285325
}
286326
}
287327
}
288-
const returnValue = removeBatch(['foo', [1, 2]], state, { changeValue })
289-
expect(returnValue).toEqual(['b', 'c'])
328+
const returnValue = removeBatch(['foo', [1, 3]], state, { changeValue })
329+
expect(returnValue).toEqual(['b', 'd'])
290330
expect(state.formState.values.foo).not.toBe(array) // copied
291331
expect(state).toEqual({
292332
formState: {
293333
values: {
294-
foo: ['a', 'd', 'e'],
334+
foo: ['a', 'c', 'e'],
295335
anotherField: 42
296336
}
297337
},
298338
fields: {
339+
'foo[2]': {
340+
name: 'foo[2]',
341+
blur: blur2,
342+
change: change2,
343+
focus: focus2,
344+
touched: true,
345+
error: 'E Error',
346+
lastFieldState: undefined
347+
},
299348
'foo[0]': {
300349
name: 'foo[0]',
301350
blur: blur0,
@@ -310,17 +359,8 @@ describe('removeBatch', () => {
310359
blur: blur1,
311360
change: change1,
312361
focus: focus1,
313-
touched: false,
314-
error: 'D Error',
315-
lastFieldState: undefined
316-
},
317-
'foo[2]': {
318-
name: 'foo[2]',
319-
blur: blur2,
320-
change: change2,
321-
focus: focus2,
322362
touched: true,
323-
error: 'E Error',
363+
error: 'C Error',
324364
lastFieldState: undefined
325365
},
326366
anotherField: {
@@ -362,6 +402,14 @@ describe('removeBatch', () => {
362402
}
363403
},
364404
fields: {
405+
'foo[0][4]': {
406+
name: 'foo[0][4]',
407+
blur: blur4,
408+
change: change4,
409+
focus: focus4,
410+
touched: true,
411+
error: 'E Error'
412+
},
365413
'foo[0][0]': {
366414
name: 'foo[0][0]',
367415
blur: blur0,
@@ -394,33 +442,34 @@ describe('removeBatch', () => {
394442
touched: false,
395443
error: 'D Error'
396444
},
397-
'foo[0][4]': {
398-
name: 'foo[0][4]',
399-
blur: blur4,
400-
change: change4,
401-
focus: focus4,
402-
touched: true,
403-
error: 'E Error'
404-
},
405445
anotherField: {
406446
name: 'anotherField',
407447
touched: false
408448
}
409449
}
410450
}
411-
const returnValue = removeBatch(['foo[0]', [1, 2]], state, {
451+
const returnValue = removeBatch(['foo[0]', [1, 3]], state, {
412452
changeValue
413453
})
414-
expect(returnValue).toEqual(['b', 'c'])
454+
expect(returnValue).toEqual(['b', 'd'])
415455
expect(state.formState.values.foo).not.toBe(array) // copied
416456
expect(state).toEqual({
417457
formState: {
418458
values: {
419-
foo: [['a', 'd', 'e']],
459+
foo: [['a', 'c', 'e']],
420460
anotherField: 42
421461
}
422462
},
423463
fields: {
464+
'foo[0][2]': {
465+
name: 'foo[0][2]',
466+
blur: blur2,
467+
change: change2,
468+
focus: focus2,
469+
touched: true,
470+
error: 'E Error',
471+
lastFieldState: undefined
472+
},
424473
'foo[0][0]': {
425474
name: 'foo[0][0]',
426475
blur: blur0,
@@ -435,17 +484,8 @@ describe('removeBatch', () => {
435484
blur: blur1,
436485
change: change1,
437486
focus: focus1,
438-
touched: false,
439-
error: 'D Error',
440-
lastFieldState: undefined
441-
},
442-
'foo[0][2]': {
443-
name: 'foo[0][2]',
444-
blur: blur2,
445-
change: change2,
446-
focus: focus2,
447487
touched: true,
448-
error: 'E Error',
488+
error: 'C Error',
449489
lastFieldState: undefined
450490
},
451491
anotherField: {

0 commit comments

Comments
 (0)