Skip to content

Commit b197a58

Browse files
authored
Smarter field state management during insert and remove (#29)
1 parent 9345846 commit b197a58

8 files changed

+479
-60
lines changed

src/insert.js

+20
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ const insert: Mutator = (
1515
return copy
1616
}
1717
)
18+
19+
// now we have increment any higher indexes
20+
const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`)
21+
const changes = {}
22+
Object.keys(state.fields).forEach(key => {
23+
const tokens = pattern.exec(key)
24+
if (tokens) {
25+
const fieldIndex = Number(tokens[1])
26+
if (fieldIndex >= index) {
27+
// inc index one higher
28+
const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}`
29+
changes[incrementedKey] = state.fields[key]
30+
changes[incrementedKey].name = incrementedKey
31+
}
32+
if (fieldIndex === index) {
33+
delete state.fields[key]
34+
}
35+
}
36+
})
37+
state.fields = { ...state.fields, ...changes }
1838
}
1939

2040
export default insert

src/insert.test.js

+111-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
11
import insert from './insert'
2+
import { getIn, setIn } from 'final-form'
23

34
describe('insert', () => {
45
const getOp = (index, value) => {
56
const changeValue = jest.fn()
6-
insert(['foo', index, value], {}, { changeValue })
7+
const state = {
8+
formState: {
9+
values: {
10+
foo: ['one', 'two']
11+
}
12+
},
13+
fields: {
14+
'foo[0]': {
15+
name: 'foo[0]',
16+
touched: true,
17+
error: 'First Error'
18+
},
19+
'foo[1]': {
20+
name: 'foo[1]',
21+
touched: false,
22+
error: 'Second Error'
23+
}
24+
}
25+
}
26+
insert(['foo', index, value], state, { changeValue })
727
return changeValue.mock.calls[0][2]
828
}
929

1030
it('should call changeValue once', () => {
1131
const changeValue = jest.fn()
12-
const state = {}
32+
const state = {
33+
formState: {
34+
values: {
35+
foo: ['one', 'two']
36+
}
37+
},
38+
fields: {
39+
'foo[0]': {
40+
name: 'foo[0]',
41+
touched: true,
42+
error: 'First Error'
43+
},
44+
'foo[1]': {
45+
name: 'foo[1]',
46+
touched: false,
47+
error: 'Second Error'
48+
}
49+
}
50+
}
1351
const result = insert(['foo', 0, 'bar'], state, { changeValue })
1452
expect(result).toBeUndefined()
1553
expect(changeValue).toHaveBeenCalled()
@@ -35,4 +73,75 @@ describe('insert', () => {
3573
expect(Array.isArray(result)).toBe(true)
3674
expect(result).toEqual(['a', 'd', 'b', 'c'])
3775
})
76+
77+
it('should increment other field data from the specified index', () => {
78+
const array = ['a', 'b', 'c', 'd']
79+
// implementation of changeValue taken directly from Final Form
80+
const changeValue = (state, name, mutate) => {
81+
const before = getIn(state.formState.values, name)
82+
const after = mutate(before)
83+
state.formState.values = setIn(state.formState.values, name, after) || {}
84+
}
85+
const state = {
86+
formState: {
87+
values: {
88+
foo: array
89+
}
90+
},
91+
fields: {
92+
'foo[0]': {
93+
name: 'foo[0]',
94+
touched: true,
95+
error: 'A Error'
96+
},
97+
'foo[1]': {
98+
name: 'foo[1]',
99+
touched: false,
100+
error: 'B Error'
101+
},
102+
'foo[2]': {
103+
name: 'foo[2]',
104+
touched: true,
105+
error: 'C Error'
106+
},
107+
'foo[3]': {
108+
name: 'foo[3]',
109+
touched: false,
110+
error: 'D Error'
111+
}
112+
}
113+
}
114+
const returnValue = insert(['foo', 1, 'NEWVALUE'], state, { changeValue })
115+
expect(returnValue).toBeUndefined()
116+
expect(state.formState.values.foo).not.toBe(array) // copied
117+
expect(state).toEqual({
118+
formState: {
119+
values: {
120+
foo: ['a', 'NEWVALUE', 'b', 'c', 'd']
121+
}
122+
},
123+
fields: {
124+
'foo[0]': {
125+
name: 'foo[0]',
126+
touched: true,
127+
error: 'A Error'
128+
},
129+
'foo[2]': {
130+
name: 'foo[2]',
131+
touched: false,
132+
error: 'B Error'
133+
},
134+
'foo[3]': {
135+
name: 'foo[3]',
136+
touched: true,
137+
error: 'C Error'
138+
},
139+
'foo[4]': {
140+
name: 'foo[4]',
141+
touched: false,
142+
error: 'D Error'
143+
}
144+
}
145+
})
146+
})
38147
})

src/remove.js

+21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ const remove: Mutator = (
1717
return copy
1818
}
1919
)
20+
21+
// now we have to remove any subfields for our index,
22+
// and decrement all higher indexes.
23+
const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`)
24+
const backup = { ...state.fields }
25+
Object.keys(state.fields).forEach(key => {
26+
const tokens = pattern.exec(key)
27+
if (tokens) {
28+
const fieldIndex = Number(tokens[1])
29+
if (fieldIndex === index) {
30+
// delete any subfields for this array item
31+
delete state.fields[key]
32+
} else if (fieldIndex > index) {
33+
// shift all higher ones down
34+
delete state.fields[key]
35+
const decrementedKey = `${name}[${fieldIndex - 1}]${tokens[2]}`
36+
state.fields[decrementedKey] = backup[key]
37+
state.fields[decrementedKey].name = decrementedKey
38+
}
39+
}
40+
})
2041
return returnValue
2142
}
2243

src/remove.test.js

+90-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
import remove from './remove'
2+
import { getIn, setIn } from 'final-form'
23

34
describe('remove', () => {
45
it('should call changeValue once', () => {
56
const changeValue = jest.fn()
6-
const state = {}
7+
const state = {
8+
formState: {
9+
values: {
10+
foo: ['one', 'two']
11+
}
12+
},
13+
fields: {
14+
'foo[0]': {
15+
name: 'foo[0]',
16+
touched: true,
17+
error: 'First Error'
18+
},
19+
'foo[1]': {
20+
name: 'foo[1]',
21+
touched: false,
22+
error: 'Second Error'
23+
}
24+
}
25+
}
726
const result = remove(['foo', 0], state, { changeValue })
827
expect(result).toBeUndefined()
928
expect(changeValue).toHaveBeenCalled()
@@ -15,7 +34,15 @@ describe('remove', () => {
1534

1635
it('should treat undefined like an empty array', () => {
1736
const changeValue = jest.fn()
18-
const returnValue = remove(['foo', 1], {}, { changeValue })
37+
const state = {
38+
formState: {
39+
values: {
40+
foo: undefined
41+
}
42+
},
43+
fields: {}
44+
}
45+
const returnValue = remove(['foo', 1], state, { changeValue })
1946
expect(returnValue).toBeUndefined()
2047
const op = changeValue.mock.calls[0][2]
2148
const result = op(undefined)
@@ -25,14 +52,67 @@ describe('remove', () => {
2552

2653
it('should remove value from the specified index, and return it', () => {
2754
const array = ['a', 'b', 'c', 'd']
28-
let result
29-
const changeValue = jest.fn((args, state, op) => {
30-
result = op(array)
31-
})
32-
const returnValue = remove(['foo', 1], {}, { changeValue })
55+
// implementation of changeValue taken directly from Final Form
56+
const changeValue = (state, name, mutate) => {
57+
const before = getIn(state.formState.values, name)
58+
const after = mutate(before)
59+
state.formState.values = setIn(state.formState.values, name, after) || {}
60+
}
61+
const state = {
62+
formState: {
63+
values: {
64+
foo: array
65+
}
66+
},
67+
fields: {
68+
'foo[0]': {
69+
name: 'foo[0]',
70+
touched: true,
71+
error: 'A Error'
72+
},
73+
'foo[1]': {
74+
name: 'foo[1]',
75+
touched: false,
76+
error: 'B Error'
77+
},
78+
'foo[2]': {
79+
name: 'foo[2]',
80+
touched: true,
81+
error: 'C Error'
82+
},
83+
'foo[3]': {
84+
name: 'foo[3]',
85+
touched: false,
86+
error: 'D Error'
87+
}
88+
}
89+
}
90+
const returnValue = remove(['foo', 1], state, { changeValue })
3391
expect(returnValue).toBe('b')
34-
expect(result).not.toBe(array) // copied
35-
expect(Array.isArray(result)).toBe(true)
36-
expect(result).toEqual(['a', 'c', 'd'])
92+
expect(state.formState.values.foo).not.toBe(array) // copied
93+
expect(state).toEqual({
94+
formState: {
95+
values: {
96+
foo: ['a', 'c', 'd']
97+
}
98+
},
99+
fields: {
100+
'foo[0]': {
101+
name: 'foo[0]',
102+
touched: true,
103+
error: 'A Error'
104+
},
105+
'foo[1]': {
106+
name: 'foo[1]',
107+
touched: true,
108+
error: 'C Error'
109+
},
110+
'foo[2]': {
111+
name: 'foo[2]',
112+
touched: false,
113+
error: 'D Error'
114+
}
115+
}
116+
})
37117
})
38118
})

src/shift.js

+3-21
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,8 @@
11
// @flow
22
import type { MutableState, Mutator, Tools } from 'final-form'
3+
import remove from './remove'
34

4-
const shift: Mutator = (
5-
[name]: any[],
6-
state: MutableState,
7-
{ changeValue }: Tools
8-
) => {
9-
let result
10-
changeValue(
11-
state,
12-
name,
13-
(array: ?(any[])): ?(any[]) => {
14-
if (array) {
15-
if (!array.length) {
16-
return []
17-
}
18-
result = array[0]
19-
return array.slice(1, array.length)
20-
}
21-
}
22-
)
23-
return result
24-
}
5+
const shift: Mutator = ([name]: any[], state: MutableState, tools: Tools) =>
6+
remove([name, 0], state, tools)
257

268
export default shift

0 commit comments

Comments
 (0)