Skip to content

Commit 27acc9a

Browse files
SpOOnmanerikras
authored andcommitted
Improved move implementation with complex different shapes (final-form#41)
1 parent 7c9c8ef commit 27acc9a

10 files changed

+263
-73
lines changed

src/insert.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,27 @@ const insert: Mutator<any> = (
1313
return copy
1414
})
1515

16+
const backup = { ...state.fields }
17+
1618
// now we have increment any higher indexes
1719
const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`)
18-
const backup = { ...state.fields }
19-
Object.keys(state.fields).forEach(key => {
20-
const tokens = pattern.exec(key)
21-
if (tokens) {
22-
const fieldIndex = Number(tokens[1])
23-
if (fieldIndex >= index) {
24-
// inc index one higher
25-
const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}`
26-
moveFieldState(state, backup[key], incrementedKey)
27-
}
28-
if (fieldIndex === index) {
29-
resetFieldState(key)
20+
21+
// we need to increment high indices first so
22+
// lower indices won't overlap
23+
Object.keys(state.fields)
24+
.sort()
25+
.reverse()
26+
.forEach(key => {
27+
const tokens = pattern.exec(key)
28+
if (tokens) {
29+
const fieldIndex = Number(tokens[1])
30+
if (fieldIndex >= index) {
31+
// inc index one higher
32+
const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}`
33+
moveFieldState(state, backup[key], incrementedKey)
34+
}
3035
}
31-
}
32-
})
36+
})
3337
}
3438

3539
export default insert

src/insert.test.js

-6
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,6 @@ describe('insert', () => {
141141
touched: true,
142142
error: 'A Error'
143143
},
144-
'foo[1]': {
145-
name: 'foo[1]',
146-
touched: false,
147-
error: 'B Error',
148-
lastFieldState: undefined
149-
},
150144
'foo[2]': {
151145
name: 'foo[2]',
152146
touched: true,

src/move.js

+31-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// @flow
22
import type { MutableState, Mutator, Tools } from 'final-form'
3-
import moveFieldState from './moveFieldState'
3+
import moveFields from './moveFields'
4+
import restoreFunctions from './restoreFunctions'
5+
6+
const TMP: string = 'tmp'
47

58
const move: Mutator<any> = (
69
[name, from, to]: any[],
@@ -17,39 +20,35 @@ const move: Mutator<any> = (
1720
copy.splice(to, 0, value)
1821
return copy
1922
})
23+
24+
//make a copy of a state for further functions restore
25+
const backupState = { ...state, fields: { ...state.fields } }
26+
27+
// move this row to tmp index
2028
const fromPrefix = `${name}[${from}]`
21-
Object.keys(state.fields).forEach(key => {
22-
if (key.substring(0, fromPrefix.length) === fromPrefix) {
23-
const suffix = key.substring(fromPrefix.length)
24-
const fromKey = fromPrefix + suffix
25-
const backup = state.fields[fromKey]
26-
if (from < to) {
27-
// moving to a higher index
28-
// decrement all indices between from and to
29-
for (let i = from; i < to; i++) {
30-
const destKey = `${name}[${i}]${suffix}`
31-
moveFieldState(
32-
state,
33-
state.fields[`${name}[${i + 1}]${suffix}`],
34-
destKey
35-
)
36-
}
37-
} else {
38-
// moving to a lower index
39-
// increment all indices between to and from
40-
for (let i = from; i > to; i--) {
41-
const destKey = `${name}[${i}]${suffix}`
42-
moveFieldState(
43-
state,
44-
state.fields[`${name}[${i - 1}]${suffix}`],
45-
destKey
46-
)
47-
}
48-
}
49-
const toKey = `${name}[${to}]${suffix}`
50-
moveFieldState(state, backup, toKey)
29+
moveFields(name, fromPrefix, TMP, state)
30+
31+
if (from < to) {
32+
// moving to a higher index
33+
// decrement all indices between from and to
34+
for (let i = from + 1; i <= to; i++) {
35+
const innerFromPrefix = `${name}[${i}]`
36+
moveFields(name, innerFromPrefix, `${i - 1}`, state)
5137
}
52-
})
38+
} else {
39+
// moving to a lower index
40+
// increment all indices between to and from
41+
for (let i = from - 1; i >= to; i--) {
42+
const innerFromPrefix = `${name}[${i}]`
43+
moveFields(name, innerFromPrefix, `${i + 1}`, state)
44+
}
45+
}
46+
47+
// move from tmp index to destination
48+
const tmpPrefix = `${name}[${TMP}]`
49+
moveFields(name, tmpPrefix, to, state)
50+
51+
restoreFunctions(state, backupState)
5352
}
5453

5554
export default move

src/move.test.js

+115
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,121 @@ describe('move', () => {
500500
}
501501
})
502502
})
503+
it('should move fields with different complex not matching shapes', () => {
504+
// implementation of changeValue taken directly from Final Form
505+
const changeValue = (state, name, mutate) => {
506+
const before = getIn(state.formState.values, name)
507+
const after = mutate(before)
508+
state.formState.values = setIn(state.formState.values, name, after) || {}
509+
}
510+
const state = {
511+
formState: {
512+
values: {
513+
foo: [{ dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} },
514+
{ dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} }]
515+
}
516+
},
517+
fields: {
518+
'foo[0].dog': {
519+
name: 'foo[0].dog',
520+
touched: true,
521+
error: 'Error A Dog'
522+
},
523+
'foo[0].cat': {
524+
name: 'foo[0].cat',
525+
touched: false,
526+
error: 'Error A Cat'
527+
},
528+
'foo[0].colors[0].name': {
529+
name: 'foo[0].colors[0].name',
530+
touched: true,
531+
error: 'Error A Colors Red'
532+
},
533+
'foo[0].colors[1].name': {
534+
name: 'foo[0].colors[1].name',
535+
touched: true,
536+
error: 'Error A Colors Blue'
537+
},
538+
'foo[0].deep.inside.rock': {
539+
name: 'foo[0].deep.inside.rock',
540+
touched: true,
541+
error: 'Error A Deep Inside Rock Black'
542+
},
543+
'foo[1].dog': {
544+
name: 'foo[1].dog',
545+
touched: true,
546+
error: 'Error B Dog'
547+
},
548+
'foo[1].mouse': {
549+
name: 'foo[1].mouse',
550+
touched: true,
551+
error: 'Error B Mickey'
552+
},
553+
'foo[1].deep.inside.axe': {
554+
name: 'foo[1].deep.inside.axe',
555+
touched: true,
556+
error: 'Error B Deep Inside Axe Golden'
557+
},
558+
}
559+
}
560+
move(['foo', 0, 1], state, { changeValue })
561+
expect(state).toMatchObject({
562+
formState: {
563+
values: {
564+
foo: [{ dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} },
565+
{ dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} }]
566+
}
567+
},
568+
fields: {
569+
'foo[0].dog': {
570+
name: 'foo[0].dog',
571+
touched: true,
572+
error: 'Error B Dog',
573+
lastFieldState: undefined
574+
},
575+
'foo[0].mouse': {
576+
name: 'foo[0].mouse',
577+
touched: true,
578+
error: 'Error B Mickey',
579+
lastFieldState: undefined
580+
},
581+
'foo[0].deep.inside.axe': {
582+
name: 'foo[0].deep.inside.axe',
583+
touched: true,
584+
error: 'Error B Deep Inside Axe Golden'
585+
},
586+
'foo[1].dog': {
587+
name: 'foo[1].dog',
588+
touched: true,
589+
error: 'Error A Dog',
590+
lastFieldState: undefined
591+
},
592+
'foo[1].cat': {
593+
name: 'foo[1].cat',
594+
touched: false,
595+
error: 'Error A Cat',
596+
lastFieldState: undefined
597+
},
598+
'foo[1].colors[0].name': {
599+
name: 'foo[1].colors[0].name',
600+
touched: true,
601+
error: 'Error A Colors Red',
602+
lastFieldState: undefined
603+
},
604+
'foo[1].colors[1].name': {
605+
name: 'foo[1].colors[1].name',
606+
touched: true,
607+
error: 'Error A Colors Blue',
608+
lastFieldState: undefined
609+
},
610+
'foo[1].deep.inside.rock': {
611+
name: 'foo[1].deep.inside.rock',
612+
touched: true,
613+
error: 'Error A Deep Inside Rock Black'
614+
},
615+
}
616+
})
617+
})
503618

504619
it('should preserve functions in field state', () => {
505620
// implementation of changeValue taken directly from Final Form

src/moveFieldState.js

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ function moveFieldState(
77
destKey: string,
88
oldState: MutableState<any> = state
99
) {
10+
delete state.fields[source.name]
1011
state.fields[destKey] = {
1112
...source,
1213
name: destKey,
@@ -18,6 +19,15 @@ function moveFieldState(
1819
focus: oldState.fields[destKey] && oldState.fields[destKey].focus,
1920
lastFieldState: undefined // clearing lastFieldState forces renotification
2021
}
22+
if (!state.fields[destKey].change) {
23+
delete state.fields[destKey].change;
24+
}
25+
if (!state.fields[destKey].blur) {
26+
delete state.fields[destKey].blur;
27+
}
28+
if (!state.fields[destKey].focus) {
29+
delete state.fields[destKey].focus;
30+
}
2131
}
2232

2333
export default moveFieldState

src/moveFields.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @flow
2+
import type { MutableState } from 'final-form'
3+
import moveFieldState from './moveFieldState';
4+
5+
function moveFields(
6+
name: string,
7+
matchPrefix: string,
8+
destIndex: string,
9+
state: MutableState<any>
10+
) {
11+
Object.keys(state.fields).forEach(key => {
12+
if (key.substring(0, matchPrefix.length) === matchPrefix) {
13+
const suffix = key.substring(matchPrefix.length)
14+
const destKey = `${name}[${destIndex}]${suffix}`
15+
moveFieldState(state, state.fields[key], destKey)
16+
}
17+
})
18+
}
19+
20+
export default moveFields

src/restoreFunctions.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// @flow
2+
import type { MutableState } from 'final-form'
3+
4+
function restoreFunctions(
5+
state: MutableState<any>,
6+
backupState: MutableState<any>
7+
) {
8+
Object.keys(state.fields).forEach(key => {
9+
state.fields[key] = {
10+
...state.fields[key],
11+
change: state.fields[key].change || (backupState.fields[key] && backupState.fields[key].change),
12+
blur: state.fields[key].blur || (backupState.fields[key] && backupState.fields[key].blur),
13+
focus: state.fields[key].focus || (backupState.fields[key] && backupState.fields[key].focus)
14+
}
15+
if (!state.fields[key].change) {
16+
delete state.fields[key].change;
17+
}
18+
if (!state.fields[key].blur) {
19+
delete state.fields[key].blur;
20+
}
21+
if (!state.fields[key].focus) {
22+
delete state.fields[key].focus;
23+
}
24+
})
25+
}
26+
export default restoreFunctions

src/swap.js

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// @flow
22
import type { MutableState, Mutator, Tools } from 'final-form'
33
import moveFieldState from './moveFieldState'
4+
import moveFields from './moveFields';
5+
import restoreFunctions from './restoreFunctions';
6+
7+
const TMP: string = 'tmp'
48

59
const swap: Mutator<any> = (
610
[name, indexA, indexB]: any[],
@@ -17,20 +21,20 @@ const swap: Mutator<any> = (
1721
copy[indexB] = a
1822
return copy
1923
})
24+
25+
//make a copy of a state for further functions restore
26+
const backupState = { ...state, fields: { ...state.fields } }
27+
2028
// swap all field state that begin with "name[indexA]" with that under "name[indexB]"
2129
const aPrefix = `${name}[${indexA}]`
2230
const bPrefix = `${name}[${indexB}]`
23-
Object.keys(state.fields).forEach(key => {
24-
if (key.substring(0, aPrefix.length) === aPrefix) {
25-
const suffix = key.substring(aPrefix.length)
26-
const aKey = aPrefix + suffix
27-
const bKey = bPrefix + suffix
28-
const fieldA = state.fields[aKey]
31+
const tmpPrefix = `${name}[${TMP}]`
2932

30-
moveFieldState(state, state.fields[bKey], aKey)
31-
moveFieldState(state, fieldA, bKey)
32-
}
33-
})
33+
moveFields(name, aPrefix, TMP, state)
34+
moveFields(name, bPrefix, indexA, state)
35+
moveFields(name, tmpPrefix, indexB, state)
36+
37+
restoreFunctions(state, backupState)
3438
}
3539

3640
export default swap

0 commit comments

Comments
 (0)