From 32fafdffd95d4f788d6389083a82f9d3c754966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kalkosi=C5=84ski?= Date: Wed, 18 Sep 2019 16:24:01 +0200 Subject: [PATCH] Improved move implementation with complex different shapes --- src/insert.js | 32 ++++++----- src/insert.test.js | 6 --- src/move.js | 63 +++++++++++----------- src/move.test.js | 115 ++++++++++++++++++++++++++++++++++++++++ src/moveFieldState.js | 10 ++++ src/moveFields.js | 20 +++++++ src/restoreFunctions.js | 26 +++++++++ src/swap.js | 24 +++++---- src/swap.test.js | 34 ++++++++++-- src/unshift.test.js | 6 --- 10 files changed, 263 insertions(+), 73 deletions(-) create mode 100644 src/moveFields.js create mode 100644 src/restoreFunctions.js diff --git a/src/insert.js b/src/insert.js index 421f3f3..b759812 100644 --- a/src/insert.js +++ b/src/insert.js @@ -13,23 +13,27 @@ const insert: Mutator = ( return copy }) + const backup = { ...state.fields } + // now we have increment any higher indexes const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) - const backup = { ...state.fields } - Object.keys(state.fields).forEach(key => { - const tokens = pattern.exec(key) - if (tokens) { - const fieldIndex = Number(tokens[1]) - if (fieldIndex >= index) { - // inc index one higher - const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}` - moveFieldState(state, backup[key], incrementedKey) - } - if (fieldIndex === index) { - resetFieldState(key) + + // we need to increment high indices first so + // lower indices won't overlap + Object.keys(state.fields) + .sort() + .reverse() + .forEach(key => { + const tokens = pattern.exec(key) + if (tokens) { + const fieldIndex = Number(tokens[1]) + if (fieldIndex >= index) { + // inc index one higher + const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}` + moveFieldState(state, backup[key], incrementedKey) + } } - } - }) + }) } export default insert diff --git a/src/insert.test.js b/src/insert.test.js index 78cde4a..b432816 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -141,12 +141,6 @@ describe('insert', () => { touched: true, error: 'A Error' }, - 'foo[1]': { - name: 'foo[1]', - touched: false, - error: 'B Error', - lastFieldState: undefined - }, 'foo[2]': { name: 'foo[2]', touched: true, diff --git a/src/move.js b/src/move.js index 69c4bde..4aaca6c 100644 --- a/src/move.js +++ b/src/move.js @@ -1,6 +1,9 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import moveFields from './moveFields' +import restoreFunctions from './restoreFunctions' + +const TMP: string = 'tmp' const move: Mutator = ( [name, from, to]: any[], @@ -17,39 +20,35 @@ const move: Mutator = ( copy.splice(to, 0, value) return copy }) + + //make a copy of a state for further functions restore + const backupState = { ...state, fields: { ...state.fields } } + + // move this row to tmp index const fromPrefix = `${name}[${from}]` - Object.keys(state.fields).forEach(key => { - if (key.substring(0, fromPrefix.length) === fromPrefix) { - const suffix = key.substring(fromPrefix.length) - const fromKey = fromPrefix + suffix - const backup = state.fields[fromKey] - if (from < to) { - // moving to a higher index - // decrement all indices between from and to - for (let i = from; i < to; i++) { - const destKey = `${name}[${i}]${suffix}` - moveFieldState( - state, - state.fields[`${name}[${i + 1}]${suffix}`], - destKey - ) - } - } else { - // moving to a lower index - // increment all indices between to and from - for (let i = from; i > to; i--) { - const destKey = `${name}[${i}]${suffix}` - moveFieldState( - state, - state.fields[`${name}[${i - 1}]${suffix}`], - destKey - ) - } - } - const toKey = `${name}[${to}]${suffix}` - moveFieldState(state, backup, toKey) + moveFields(name, fromPrefix, TMP, state) + + if (from < to) { + // moving to a higher index + // decrement all indices between from and to + for (let i = from + 1; i <= to; i++) { + const innerFromPrefix = `${name}[${i}]` + moveFields(name, innerFromPrefix, `${i - 1}`, state) } - }) + } else { + // moving to a lower index + // increment all indices between to and from + for (let i = from - 1; i >= to; i--) { + const innerFromPrefix = `${name}[${i}]` + moveFields(name, innerFromPrefix, `${i + 1}`, state) + } + } + + // move from tmp index to destination + const tmpPrefix = `${name}[${TMP}]` + moveFields(name, tmpPrefix, to, state) + + restoreFunctions(state, backupState) } export default move diff --git a/src/move.test.js b/src/move.test.js index 68ce2ad..91044c2 100644 --- a/src/move.test.js +++ b/src/move.test.js @@ -500,6 +500,121 @@ describe('move', () => { } }) }) + it('should move fields with different complex not matching shapes', () => { + // implementation of changeValue taken directly from Final Form + const changeValue = (state, name, mutate) => { + const before = getIn(state.formState.values, name) + const after = mutate(before) + state.formState.values = setIn(state.formState.values, name, after) || {} + } + const state = { + formState: { + values: { + foo: [{ dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} }, + { dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} }] + } + }, + fields: { + 'foo[0].dog': { + name: 'foo[0].dog', + touched: true, + error: 'Error A Dog' + }, + 'foo[0].cat': { + name: 'foo[0].cat', + touched: false, + error: 'Error A Cat' + }, + 'foo[0].colors[0].name': { + name: 'foo[0].colors[0].name', + touched: true, + error: 'Error A Colors Red' + }, + 'foo[0].colors[1].name': { + name: 'foo[0].colors[1].name', + touched: true, + error: 'Error A Colors Blue' + }, + 'foo[0].deep.inside.rock': { + name: 'foo[0].deep.inside.rock', + touched: true, + error: 'Error A Deep Inside Rock Black' + }, + 'foo[1].dog': { + name: 'foo[1].dog', + touched: true, + error: 'Error B Dog' + }, + 'foo[1].mouse': { + name: 'foo[1].mouse', + touched: true, + error: 'Error B Mickey' + }, + 'foo[1].deep.inside.axe': { + name: 'foo[1].deep.inside.axe', + touched: true, + error: 'Error B Deep Inside Axe Golden' + }, + } + } + move(['foo', 0, 1], state, { changeValue }) + expect(state).toMatchObject({ + formState: { + values: { + foo: [{ dog: 'banana dog', mouse: 'mickey', deep: { inside: { axe: 'golden' }} }, + { dog: 'apple dog', cat: 'apple cat', colors: [{ name: 'red'}, { name: 'blue'}], deep: { inside: { rock: 'black'}} }] + } + }, + fields: { + 'foo[0].dog': { + name: 'foo[0].dog', + touched: true, + error: 'Error B Dog', + lastFieldState: undefined + }, + 'foo[0].mouse': { + name: 'foo[0].mouse', + touched: true, + error: 'Error B Mickey', + lastFieldState: undefined + }, + 'foo[0].deep.inside.axe': { + name: 'foo[0].deep.inside.axe', + touched: true, + error: 'Error B Deep Inside Axe Golden' + }, + 'foo[1].dog': { + name: 'foo[1].dog', + touched: true, + error: 'Error A Dog', + lastFieldState: undefined + }, + 'foo[1].cat': { + name: 'foo[1].cat', + touched: false, + error: 'Error A Cat', + lastFieldState: undefined + }, + 'foo[1].colors[0].name': { + name: 'foo[1].colors[0].name', + touched: true, + error: 'Error A Colors Red', + lastFieldState: undefined + }, + 'foo[1].colors[1].name': { + name: 'foo[1].colors[1].name', + touched: true, + error: 'Error A Colors Blue', + lastFieldState: undefined + }, + 'foo[1].deep.inside.rock': { + name: 'foo[1].deep.inside.rock', + touched: true, + error: 'Error A Deep Inside Rock Black' + }, + } + }) + }) it('should preserve functions in field state', () => { // implementation of changeValue taken directly from Final Form diff --git a/src/moveFieldState.js b/src/moveFieldState.js index 56c93dc..c1219ea 100644 --- a/src/moveFieldState.js +++ b/src/moveFieldState.js @@ -7,6 +7,7 @@ function moveFieldState( destKey: string, oldState: MutableState = state ) { + delete state.fields[source.name] state.fields[destKey] = { ...source, name: destKey, @@ -18,6 +19,15 @@ function moveFieldState( focus: oldState.fields[destKey] && oldState.fields[destKey].focus, lastFieldState: undefined // clearing lastFieldState forces renotification } + if (!state.fields[destKey].change) { + delete state.fields[destKey].change; + } + if (!state.fields[destKey].blur) { + delete state.fields[destKey].blur; + } + if (!state.fields[destKey].focus) { + delete state.fields[destKey].focus; + } } export default moveFieldState diff --git a/src/moveFields.js b/src/moveFields.js new file mode 100644 index 0000000..c1a6203 --- /dev/null +++ b/src/moveFields.js @@ -0,0 +1,20 @@ +// @flow +import type { MutableState } from 'final-form' +import moveFieldState from './moveFieldState'; + +function moveFields( + name: string, + matchPrefix: string, + destIndex: string, + state: MutableState +) { + Object.keys(state.fields).forEach(key => { + if (key.substring(0, matchPrefix.length) === matchPrefix) { + const suffix = key.substring(matchPrefix.length) + const destKey = `${name}[${destIndex}]${suffix}` + moveFieldState(state, state.fields[key], destKey) + } + }) +} + +export default moveFields diff --git a/src/restoreFunctions.js b/src/restoreFunctions.js new file mode 100644 index 0000000..5a21cc9 --- /dev/null +++ b/src/restoreFunctions.js @@ -0,0 +1,26 @@ +// @flow +import type { MutableState } from 'final-form' + +function restoreFunctions( + state: MutableState, + backupState: MutableState +) { + Object.keys(state.fields).forEach(key => { + state.fields[key] = { + ...state.fields[key], + change: state.fields[key].change || (backupState.fields[key] && backupState.fields[key].change), + blur: state.fields[key].blur || (backupState.fields[key] && backupState.fields[key].blur), + focus: state.fields[key].focus || (backupState.fields[key] && backupState.fields[key].focus) + } + if (!state.fields[key].change) { + delete state.fields[key].change; + } + if (!state.fields[key].blur) { + delete state.fields[key].blur; + } + if (!state.fields[key].focus) { + delete state.fields[key].focus; + } + }) +} +export default restoreFunctions diff --git a/src/swap.js b/src/swap.js index 0b33d40..e51fe25 100644 --- a/src/swap.js +++ b/src/swap.js @@ -1,6 +1,10 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' import moveFieldState from './moveFieldState' +import moveFields from './moveFields'; +import restoreFunctions from './restoreFunctions'; + +const TMP: string = 'tmp' const swap: Mutator = ( [name, indexA, indexB]: any[], @@ -17,20 +21,20 @@ const swap: Mutator = ( copy[indexB] = a return copy }) + + //make a copy of a state for further functions restore + const backupState = { ...state, fields: { ...state.fields } } + // swap all field state that begin with "name[indexA]" with that under "name[indexB]" const aPrefix = `${name}[${indexA}]` const bPrefix = `${name}[${indexB}]` - Object.keys(state.fields).forEach(key => { - if (key.substring(0, aPrefix.length) === aPrefix) { - const suffix = key.substring(aPrefix.length) - const aKey = aPrefix + suffix - const bKey = bPrefix + suffix - const fieldA = state.fields[aKey] + const tmpPrefix = `${name}[${TMP}]` - moveFieldState(state, state.fields[bKey], aKey) - moveFieldState(state, fieldA, bKey) - } - }) + moveFields(name, aPrefix, TMP, state) + moveFields(name, bPrefix, indexA, state) + moveFields(name, tmpPrefix, indexB, state) + + restoreFunctions(state, backupState) } export default swap diff --git a/src/swap.test.js b/src/swap.test.js index e1b0bc0..013940a 100644 --- a/src/swap.test.js +++ b/src/swap.test.js @@ -121,7 +121,7 @@ describe('swap', () => { }) }) - it('should swap field state for deep fields', () => { + it('should swap field state for deep fields and different shapes', () => { // implementation of changeValue taken directly from Final Form const changeValue = (state, name, mutate) => { const before = getIn(state.formState.values, name) @@ -132,9 +132,9 @@ describe('swap', () => { formState: { values: { foo: [ - { dog: 'apple dog', cat: 'apple cat' }, + { dog: 'apple dog', cat: 'apple cat', rock: 'black' }, { dog: 'banana dog', cat: 'banana cat' }, - { dog: 'carrot dog', cat: 'carrot cat' }, + { dog: 'carrot dog', cat: 'carrot cat', axe: 'golden' }, { dog: 'date dog', cat: 'date cat' } ] } @@ -152,6 +152,12 @@ describe('swap', () => { error: 'Error A Cat', lastFieldState: 'anything' }, + 'foo[0].rock': { + name: 'foo[0].rock', + touched: false, + error: 'Error A Rock', + lastFieldState: 'anything' + }, 'foo[1].dog': { name: 'foo[1].dog', touched: true, @@ -176,6 +182,12 @@ describe('swap', () => { error: 'Error C Cat', lastFieldState: 'anything' }, + 'foo[2].axe': { + name: 'foo[2].axe', + touched: false, + error: 'Error C Axe', + lastFieldState: 'anything' + }, 'foo[3].dog': { name: 'foo[3].dog', touched: false, @@ -195,9 +207,9 @@ describe('swap', () => { formState: { values: { foo: [ - { dog: 'carrot dog', cat: 'carrot cat' }, + { dog: 'carrot dog', cat: 'carrot cat', axe: 'golden' }, { dog: 'banana dog', cat: 'banana cat' }, - { dog: 'apple dog', cat: 'apple cat' }, + { dog: 'apple dog', cat: 'apple cat', rock: 'black' }, { dog: 'date dog', cat: 'date cat' } ] } @@ -215,6 +227,12 @@ describe('swap', () => { error: 'Error C Cat', lastFieldState: undefined }, + 'foo[0].axe': { + name: 'foo[0].axe', + touched: false, + error: 'Error C Axe', + lastFieldState: undefined + }, 'foo[1].dog': { name: 'foo[1].dog', touched: true, @@ -239,6 +257,12 @@ describe('swap', () => { error: 'Error A Cat', lastFieldState: undefined }, + 'foo[2].rock': { + name: 'foo[2].rock', + touched: false, + error: 'Error A Rock', + lastFieldState: undefined + }, 'foo[3].dog': { name: 'foo[3].dog', touched: false, diff --git a/src/unshift.test.js b/src/unshift.test.js index cb2dd7d..0dbf91b 100644 --- a/src/unshift.test.js +++ b/src/unshift.test.js @@ -118,12 +118,6 @@ describe('unshift', () => { } }, fields: { - 'foo[0]': { - name: 'foo[0]', - touched: false, - error: 'A Error', - lastFieldState: undefined - }, 'foo[1]': { name: 'foo[1]', touched: true,