diff --git a/src/copyField.js b/src/copyField.js new file mode 100644 index 0000000..33d3bb1 --- /dev/null +++ b/src/copyField.js @@ -0,0 +1,35 @@ +// @flow +import type { InternalFieldState } from 'final-form/dist/types' + +function copyField( + oldFields: { [string]: InternalFieldState }, + oldKey: string, + newFields: { [string]: InternalFieldState }, + newKey: string +) { + newFields[newKey] = { + ...oldFields[oldKey], + name: newKey, + // prevent functions from being overwritten + // if the newFields[newKey] does not exist, it will be created + // when that field gets registered, with its own change/blur/focus callbacks + change: oldFields[newKey] && oldFields[newKey].change, + blur: oldFields[newKey] && oldFields[newKey].blur, + focus: oldFields[newKey] && oldFields[newKey].focus, + lastFieldState: undefined // clearing lastFieldState forces renotification + } + + if (!newFields[newKey].change) { + delete newFields[newKey].change + } + + if (!newFields[newKey].blur) { + delete newFields[newKey].blur + } + + if (!newFields[newKey].focus) { + delete newFields[newKey].focus + } +} + +export default copyField diff --git a/src/insert.js b/src/insert.js index 7636cee..89a0443 100644 --- a/src/insert.js +++ b/src/insert.js @@ -1,12 +1,12 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' const insert: Mutator = ( [name, index, value]: any[], state: MutableState, - { changeValue, resetFieldState }: Tools + { changeValue }: Tools ) => { changeValue(state, name, (array: ?(any[])): any[] => { const copy = [...(array || [])] @@ -14,27 +14,27 @@ const insert: Mutator = ( return copy }) - const backup = { ...state.fields } - // now we have increment any higher indexes const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - - // 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) - } + const newFields = {} + Object.keys(state.fields).forEach(key => { + const tokens = pattern.exec(key) + if (tokens) { + const fieldIndex = Number(tokens[1]) + if (fieldIndex >= index) { + // Shift all higher indices up + const incrementedKey = `${name}[${fieldIndex + 1}]${tokens[2]}` + copyField(state.fields, key, newFields, incrementedKey) + return } - }) + } + + // Keep this field that does not match the name, + // or has index smaller than what is being inserted + newFields[key] = state.fields[key] + }) + + state.fields = newFields } export default insert diff --git a/src/insert.test.js b/src/insert.test.js index d35d094..41663fd 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -84,7 +84,7 @@ describe('insert', () => { }) it('should increment other field data from the specified index', () => { - const array = ['a', 'b', 'c', 'd'] + const array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] // implementation of changeValue taken directly from Final Form const changeValue = (state, name, mutate) => { const before = getIn(state.formState.values, name) @@ -111,15 +111,15 @@ describe('insert', () => { touched: true, error: 'B Error' }, - 'foo[2]': { - name: 'foo[2]', + 'foo[9]': { + name: 'foo[9]', touched: true, - error: 'C Error' + error: 'J Error' }, - 'foo[3]': { - name: 'foo[3]', + 'foo[10]': { + name: 'foo[10]', touched: false, - error: 'D Error' + error: 'K Error' } } } @@ -132,7 +132,20 @@ describe('insert', () => { expect(state).toEqual({ formState: { values: { - foo: ['a', 'NEWVALUE', 'b', 'c', 'd'] + foo: [ + 'a', + 'NEWVALUE', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k' + ] } }, fields: { @@ -147,16 +160,16 @@ describe('insert', () => { error: 'B Error', lastFieldState: undefined }, - 'foo[3]': { - name: 'foo[3]', + 'foo[10]': { + name: 'foo[10]', touched: true, - error: 'C Error', + error: 'J Error', lastFieldState: undefined }, - 'foo[4]': { - name: 'foo[4]', + 'foo[11]': { + name: 'foo[11]', touched: false, - error: 'D Error', + error: 'K Error', lastFieldState: undefined } } diff --git a/src/move.js b/src/move.js index 4aaca6c..1529569 100644 --- a/src/move.js +++ b/src/move.js @@ -1,9 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFields from './moveFields' -import restoreFunctions from './restoreFunctions' - -const TMP: string = 'tmp' +import copyField from './copyField' +import { escapeRegexTokens } from './utils' const move: Mutator = ( [name, from, to]: any[], @@ -21,34 +19,44 @@ const move: Mutator = ( 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}]` - 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) - } + const newFields = {} + const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) + let lowest + let highest + let increment + if (from > to) { + lowest = to + highest = from + increment = 1 } 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) - } + lowest = from + highest = to + increment = -1 } + Object.keys(state.fields).forEach(key => { + const tokens = pattern.exec(key) + if (tokens) { + const fieldIndex = Number(tokens[1]) + if (fieldIndex === from) { + const newKey = `${name}[${to}]${tokens[2]}` + copyField(state.fields, key, newFields, newKey) + return + } + + if (lowest <= fieldIndex && fieldIndex <= highest) { + // Shift all indices + const newKey = `${name}[${fieldIndex + increment}]${tokens[2]}` + copyField(state.fields, key, newFields, newKey) + return + } + } - // move from tmp index to destination - const tmpPrefix = `${name}[${TMP}]` - moveFields(name, tmpPrefix, to, state) + // Keep this field that does not match the name, + // or has index smaller or larger than affected range + newFields[key] = state.fields[key] + }) - restoreFunctions(state, backupState) + state.fields = newFields } export default move diff --git a/src/moveFieldState.js b/src/moveFieldState.js deleted file mode 100644 index c1219ea..0000000 --- a/src/moveFieldState.js +++ /dev/null @@ -1,33 +0,0 @@ -// @flow -import type { MutableState } from 'final-form' - -function moveFieldState( - state: MutableState, - source: Object, - destKey: string, - oldState: MutableState = state -) { - delete state.fields[source.name] - state.fields[destKey] = { - ...source, - name: destKey, - // prevent functions from being overwritten - // if the state.fields[destKey] does not exist, it will be created - // when that field gets registered, with its own change/blur/focus callbacks - change: oldState.fields[destKey] && oldState.fields[destKey].change, - blur: oldState.fields[destKey] && oldState.fields[destKey].blur, - 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 deleted file mode 100644 index c1a6203..0000000 --- a/src/moveFields.js +++ /dev/null @@ -1,20 +0,0 @@ -// @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/pop.js b/src/pop.js index 57fe265..cfdb486 100644 --- a/src/pop.js +++ b/src/pop.js @@ -1,37 +1,17 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import { escapeRegexTokens } from './utils' +import remove from './remove' const pop: Mutator = ( [name]: any[], state: MutableState, - { changeValue }: Tools + tools: Tools ) => { - let result - let removedIndex: ?number - changeValue(state, name, (array: ?(any[])): ?(any[]) => { - if (array) { - if (!array.length) { - return [] - } - removedIndex = array.length - 1 - result = array[removedIndex] - return array.slice(0, removedIndex) - } - }) - - // now we have to remove any subfields for our index, - if (removedIndex !== undefined) { - const pattern = new RegExp( - `^${escapeRegexTokens(name)}\\[${removedIndex}].*` - ) - Object.keys(state.fields).forEach(key => { - if (pattern.test(key)) { - delete state.fields[key] - } - }) - } - return result + const { getIn } = tools; + const array = getIn(state.formState.values, name) + return array && array.length > 0 + ? remove([name, array.length - 1], state, tools) + : undefined } export default pop diff --git a/src/pop.test.js b/src/pop.test.js index f1a8b77..adfd1da 100644 --- a/src/pop.test.js +++ b/src/pop.test.js @@ -23,7 +23,7 @@ describe('pop', () => { } } } - const result = pop(['foo'], state, { changeValue }) + const result = pop(['foo'], state, { changeValue, getIn, setIn }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -33,71 +33,59 @@ describe('pop', () => { }) it('should return undefined if array is undefined', () => { - const changeValue = jest.fn() + // 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: ['one', 'two'] + foo: undefined } }, - fields: { - 'foo[0]': { - name: 'foo[0]', - touched: true, - error: 'First Error' - }, - 'foo[1]': { - name: 'foo[1]', - touched: false, - error: 'Second Error' - } - } + fields: {} } - const returnValue = pop(['foo'], state, { changeValue }) - const op = changeValue.mock.calls[0][2] + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() - const result = op(undefined) + const result = state.formState.foo expect(result).toBeUndefined() }) it('should return empty array if array is empty', () => { - const changeValue = jest.fn() + // 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: ['one', 'two'] + foo: [] } }, - fields: { - 'foo[0]': { - name: 'foo[0]', - touched: true, - error: 'First Error' - }, - 'foo[1]': { - name: 'foo[1]', - touched: false, - error: 'Second Error' - } - } + fields: {} } - const returnValue = pop(['foo'], state, { changeValue }) - const op = changeValue.mock.calls[0][2] + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBeUndefined() - const result = op([]) + const result = state.formState.values.foo expect(Array.isArray(result)).toBe(true) expect(result.length).toBe(0) }) it('should pop value off the end of array and return it', () => { - let result - const changeValue = jest.fn((args, state, op) => { - result = op(['a', 'b', 'c']) - }) + // implementation of changeValue taken directly from Final Form + const changeValue = jest.fn((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: ['one', 'two'] + foo: ['a', 'b', 'c'] } }, fields: { @@ -113,7 +101,8 @@ describe('pop', () => { } } } - const returnValue = pop(['foo'], state, { changeValue }) + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) + const result = state.formState.values.foo expect(returnValue).toBe('c') expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'b']) @@ -161,7 +150,7 @@ describe('pop', () => { } } } - const returnValue = pop(['foo'], state, { changeValue }) + const returnValue = pop(['foo'], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied @@ -238,7 +227,7 @@ describe('pop', () => { } } } - const returnValue = pop(['foo[0]'], state, { changeValue }) + const returnValue = pop(['foo[0]'], state, { changeValue, getIn, setIn }) expect(returnValue).toBe('d') expect(Array.isArray(state.formState.values.foo)).toBe(true) expect(state.formState.values.foo).not.toBe(array) // copied diff --git a/src/remove.js b/src/remove.js index 887c906..dce9a24 100644 --- a/src/remove.js +++ b/src/remove.js @@ -1,32 +1,36 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' const remove: Mutator = ( [name, index]: any[], state: MutableState, - { changeValue, renameField, getIn, setIn }: Tools + { changeValue, getIn, setIn }: Tools ) => { let returnValue - changeValue(state, name, (array: ?(any[])): any[] => { - const copy = [...(array || [])] + changeValue(state, name, (array: ?(any[])): ?(any[]) => { + if (!array) { + return array + } + + const copy = [...array] returnValue = copy[index] copy.splice(index, 1) - return copy + return copy.length > 0 + ? copy + : undefined }) // now we have to remove any subfields for our index, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const backup = { ...state, fields: { ...state.fields } } + const newFields = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { const fieldIndex = Number(tokens[1]) if (fieldIndex === index) { - // delete any subfields for this array item - delete state.fields[key] // delete any submitErrors for this array item // if the root key of the delete index if (key === `${name}[${index}]`) { @@ -38,19 +42,24 @@ const remove: Mutator = ( state = setIn(state, path, submitErrors) } } - } else if (fieldIndex > index) { - // shift all higher ones down - delete state.fields[key] + + return + } + + if (fieldIndex > index) { + // Shift all higher indices down const decrementedKey = `${name}[${fieldIndex - 1}]${tokens[2]}` - if (backup.fields[decrementedKey]) { - moveFieldState(state, backup.fields[key], decrementedKey, backup) - } else { - // take care of setting the correct change, blur, focus, validators on new field - renameField(state, key, decrementedKey) - } + copyField(state.fields, key, newFields, decrementedKey) + return } } + + // Keep this field that does not match the name, + // or has index smaller than what is being removed + newFields[key] = state.fields[key] }) + + state.fields = newFields return returnValue } diff --git a/src/remove.test.js b/src/remove.test.js index e103d7d..245f4ff 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -46,8 +46,7 @@ describe('remove', () => { expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) - expect(Array.isArray(result)).toBe(true) - expect(result.length).toBe(0) + expect(result).toBeUndefined() }) it('should remove value from the specified index, and return it', () => { @@ -94,14 +93,6 @@ describe('remove', () => { touched: false, error: 'B Error' }, - 'foo[2]': { - name: 'foo[2]', - blur: blur2, - change: change2, - focus: focus2, - touched: true, - error: 'C Error' - }, 'foo[3]': { name: 'foo[3]', blur: blur3, @@ -110,6 +101,14 @@ describe('remove', () => { touched: false, error: 'D Error' }, + 'foo[2]': { + name: 'foo[2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'C Error' + }, anotherField: { name: 'anotherField', touched: false @@ -135,15 +134,6 @@ describe('remove', () => { touched: true, error: 'A Error' }, - 'foo[1]': { - name: 'foo[1]', - blur: blur1, - change: change1, - focus: focus1, - touched: true, - error: 'C Error', - lastFieldState: undefined - }, 'foo[2]': { name: 'foo[2]', blur: blur2, @@ -153,6 +143,15 @@ describe('remove', () => { error: 'D Error', lastFieldState: undefined }, + 'foo[1]': { + name: 'foo[1]', + blur: blur1, + change: change1, + focus: focus1, + touched: true, + error: 'C Error', + lastFieldState: undefined + }, anotherField: { name: 'anotherField', touched: false @@ -189,6 +188,14 @@ describe('remove', () => { } }, fields: { + 'foo[0][3]': { + name: 'foo[0][3]', + blur: blur3, + change: change3, + focus: focus3, + touched: false, + error: 'D Error' + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -213,14 +220,6 @@ describe('remove', () => { touched: true, error: 'C Error' }, - 'foo[0][3]': { - name: 'foo[0][3]', - blur: blur3, - change: change3, - focus: focus3, - touched: false, - error: 'D Error' - }, anotherField: { name: 'anotherField', touched: false @@ -242,23 +241,6 @@ describe('remove', () => { } }, fields: { - 'foo[0][0]': { - name: 'foo[0][0]', - blur: blur0, - change: change0, - focus: focus0, - touched: true, - error: 'A Error' - }, - 'foo[0][1]': { - name: 'foo[0][1]', - blur: blur1, - change: change1, - focus: focus1, - touched: true, - error: 'C Error', - lastFieldState: undefined - }, 'foo[0][2]': { name: 'foo[0][2]', blur: blur2, @@ -268,76 +250,29 @@ describe('remove', () => { error: 'D Error', lastFieldState: undefined }, - anotherField: { - name: 'anotherField', - touched: false - } - } - }) - }) - - it('should remove value from the specified index, and handle new fields', () => { - const array = ['a', { key: 'val' }] - const changeValue = jest.fn() - const renameField = jest.fn() - function blur0() {} - function change0() {} - function focus0() {} - function blur1() {} - function change1() {} - function focus1() {} - function blur2() {} - function change2() {} - function focus2() {} - const state = { - formState: { - values: { - foo: array, - anotherField: 42 - } - }, - fields: { - 'foo[0]': { - name: 'foo[0]', + 'foo[0][0]': { + name: 'foo[0][0]', blur: blur0, change: change0, focus: focus0, touched: true, error: 'A Error' }, - 'foo[1]': { - name: 'foo[1]', + 'foo[0][1]': { + name: 'foo[0][1]', blur: blur1, change: change1, focus: focus1, - touched: false, - error: 'B Error' - }, - 'foo[1].key': { - name: 'foo[1].key', - blur: blur2, - change: change2, - focus: focus2, - touched: false, - error: 'B Error' + touched: true, + error: 'C Error', + lastFieldState: undefined }, anotherField: { name: 'anotherField', touched: false } } - } - const returnValue = remove(['foo', 0], state, { - renameField, - changeValue, - getIn, - setIn }) - expect(returnValue).toBeUndefined() - expect(renameField).toHaveBeenCalledTimes(1) - expect(renameField.mock.calls[0][0]).toEqual(state) - expect(renameField.mock.calls[0][1]).toEqual('foo[1].key') - expect(renameField.mock.calls[0][2]).toEqual('foo[0].key') }) it('should remove value from the specified index with submitError if one error in array', () => { diff --git a/src/removeBatch.js b/src/removeBatch.js index 8c2480e..48057f1 100644 --- a/src/removeBatch.js +++ b/src/removeBatch.js @@ -1,22 +1,46 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -import moveFieldState from './moveFieldState' +import copyField from './copyField' import { escapeRegexTokens } from './utils' -const countBelow = (array, value) => - array.reduce((count, item) => (item < value ? count + 1 : count), 0) +const binarySearch = (list: number[], value: number): number => { + // This algorithm assumes the items in list is unique + let first = 0 + let last = list.length - 1 + let middle = 0 + + while (first <= last) { + middle = Math.floor((first + last) / 2) + if (list[middle] === value) { + return middle + } + + if (list[middle] > value) { + last = middle - 1 + } else { + first = middle + 1 + } + } + + return ~first +} const removeBatch: Mutator = ( [name, indexes]: any[], state: MutableState, { changeValue }: Tools ) => { + if (indexes.length === 0) { + return [] + } + const sortedIndexes: number[] = [...indexes] sortedIndexes.sort() - // remove duplicates - for (let i = 0; i < sortedIndexes.length; i++) { - if (i > 0 && sortedIndexes[i] === sortedIndexes[i - 1]) { - sortedIndexes.splice(i--, 1) + + // Remove duplicates + for (let i = sortedIndexes.length - 1; i > 0; i -= 1) { + if (sortedIndexes[i] === sortedIndexes[i - 1]) { + sortedIndexes.splice(i, 1) } } @@ -24,39 +48,52 @@ const removeBatch: Mutator = ( changeValue(state, name, (array: ?(any[])): ?(any[]) => { // use original order of indexes for return value returnValue = indexes.map(index => array && array[index]) - if (!array || !sortedIndexes.length) { + + if (!array) { return array } const copy = [...array] - const removed = [] - sortedIndexes.forEach((index: number) => { - copy.splice(index - removed.length, 1) - removed.push(array && array[index]) - }) - return copy + for (let i = sortedIndexes.length - 1; i >= 0; i -= 1) { + const index = sortedIndexes[i] + copy.splice(index, 1) + } + + return copy.length > 0 + ? copy + : undefined }) // now we have to remove any subfields for our indexes, // and decrement all higher indexes. const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) - const newState = { ...state, fields: {} } + const newFields = {} Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) if (tokens) { const fieldIndex = Number(tokens[1]) - if (!~sortedIndexes.indexOf(fieldIndex)) { - // not one of the removed indexes - // shift all higher ones down - const decrementedKey = `${name}[${fieldIndex - - countBelow(sortedIndexes, fieldIndex)}]${tokens[2]}` - moveFieldState(newState, state.fields[key], decrementedKey, state) + const indexOfFieldIndex = binarySearch(sortedIndexes, fieldIndex) + if (indexOfFieldIndex >= 0) { + // One of the removed indices + return + } + + if (fieldIndex > sortedIndexes[0]) { + // Shift all higher indices down + const decrementedKey = `${name}[${fieldIndex - ~indexOfFieldIndex}]${ + tokens[2] + }` + copyField(state.fields, key, newFields, decrementedKey) + return } - } else { - newState.fields[key] = state.fields[key] } + + // Keep this field that does not match the name, + // or has index smaller than what is being removed + newFields[key] = state.fields[key] }) - state.fields = newState.fields + + state.fields = newFields return returnValue } diff --git a/src/removeBatch.test.js b/src/removeBatch.test.js index 15f0c8d..aaec2e5 100644 --- a/src/removeBatch.test.js +++ b/src/removeBatch.test.js @@ -187,11 +187,51 @@ describe('removeBatch', () => { expect(result).toBeUndefined() }) - it('should return the original array if no indexes are specified to be removed', () => { - const op = getOp([]) - const result = op(['a', 'b', 'c', 'd', 'e']) - expect(Array.isArray(result)).toBe(true) - expect(result).toEqual(['a', 'b', 'c', 'd', 'e']) + it('should keep the original state if no indexes are specified to be removed', () => { + const array = ['a', 'b', 'c', 'd', 'e'] + function blur0() {} + function change0() {} + function focus0() {} + const state = { + formState: { + values: { + foo: array + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error' + } + } + } + const changeValue = jest.fn() + const returnValue = removeBatch(['foo[0]', []], state, { + changeValue + }) + expect(returnValue).toEqual([]) + expect(state.formState.values.foo).toBe(array) // no change + expect(state).toEqual({ + formState: { + values: { + foo: array + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error' + } + } + }) }) it('should remove the values at the specified indexes', () => { @@ -239,6 +279,14 @@ describe('removeBatch', () => { } }, fields: { + 'foo[4]': { + name: 'foo[4]', + blur: blur4, + change: change4, + focus: focus4, + touched: true, + error: 'E Error' + }, 'foo[0]': { name: 'foo[0]', blur: blur0, @@ -271,31 +319,32 @@ describe('removeBatch', () => { touched: false, error: 'D Error' }, - 'foo[4]': { - name: 'foo[4]', - blur: blur4, - change: change4, - focus: focus4, - touched: true, - error: 'E Error' - }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo', [1, 2]], state, { changeValue }) - expect(returnValue).toEqual(['b', 'c']) + const returnValue = removeBatch(['foo', [1, 3]], state, { changeValue }) + expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ formState: { values: { - foo: ['a', 'd', 'e'], + foo: ['a', 'c', 'e'], anotherField: 42 } }, fields: { + 'foo[2]': { + name: 'foo[2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'E Error', + lastFieldState: undefined + }, 'foo[0]': { name: 'foo[0]', blur: blur0, @@ -310,17 +359,8 @@ describe('removeBatch', () => { blur: blur1, change: change1, focus: focus1, - touched: false, - error: 'D Error', - lastFieldState: undefined - }, - 'foo[2]': { - name: 'foo[2]', - blur: blur2, - change: change2, - focus: focus2, touched: true, - error: 'E Error', + error: 'C Error', lastFieldState: undefined }, anotherField: { @@ -362,6 +402,14 @@ describe('removeBatch', () => { } }, fields: { + 'foo[0][4]': { + name: 'foo[0][4]', + blur: blur4, + change: change4, + focus: focus4, + touched: true, + error: 'E Error' + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -394,33 +442,34 @@ describe('removeBatch', () => { touched: false, error: 'D Error' }, - 'foo[0][4]': { - name: 'foo[0][4]', - blur: blur4, - change: change4, - focus: focus4, - touched: true, - error: 'E Error' - }, anotherField: { name: 'anotherField', touched: false } } } - const returnValue = removeBatch(['foo[0]', [1, 2]], state, { + const returnValue = removeBatch(['foo[0]', [1, 3]], state, { changeValue }) - expect(returnValue).toEqual(['b', 'c']) + expect(returnValue).toEqual(['b', 'd']) expect(state.formState.values.foo).not.toBe(array) // copied expect(state).toEqual({ formState: { values: { - foo: [['a', 'd', 'e']], + foo: [['a', 'c', 'e']], anotherField: 42 } }, fields: { + 'foo[0][2]': { + name: 'foo[0][2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'E Error', + lastFieldState: undefined + }, 'foo[0][0]': { name: 'foo[0][0]', blur: blur0, @@ -435,17 +484,8 @@ describe('removeBatch', () => { blur: blur1, change: change1, focus: focus1, - touched: false, - error: 'D Error', - lastFieldState: undefined - }, - 'foo[0][2]': { - name: 'foo[0][2]', - blur: blur2, - change: change2, - focus: focus2, touched: true, - error: 'E Error', + error: 'C Error', lastFieldState: undefined }, anotherField: { diff --git a/src/restoreFunctions.js b/src/restoreFunctions.js deleted file mode 100644 index 5a21cc9..0000000 --- a/src/restoreFunctions.js +++ /dev/null @@ -1,26 +0,0 @@ -// @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/shift.test.js b/src/shift.test.js index 59bac3b..84950cc 100644 --- a/src/shift.test.js +++ b/src/shift.test.js @@ -46,8 +46,7 @@ describe('shift', () => { expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) - expect(Array.isArray(result)).toBe(true) - expect(result.length).toBe(0) + expect(result).toBeUndefined() }) it('should remove first value from array and return it', () => { diff --git a/src/swap.js b/src/swap.js index e51fe25..736adec 100644 --- a/src/swap.js +++ b/src/swap.js @@ -1,10 +1,6 @@ // @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' +import copyField from './copyField' const swap: Mutator = ( [name, indexA, indexB]: any[], @@ -22,19 +18,26 @@ const swap: Mutator = ( 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}]` - const tmpPrefix = `${name}[${TMP}]` - - moveFields(name, aPrefix, TMP, state) - moveFields(name, bPrefix, indexA, state) - moveFields(name, tmpPrefix, indexB, state) + const newFields = {} + Object.keys(state.fields).forEach(key => { + if (key.substring(0, aPrefix.length) === aPrefix) { + const suffix = key.substring(aPrefix.length) + const newKey = bPrefix + suffix + copyField(state.fields, key, newFields, newKey) + } else if (key.substring(0, bPrefix.length) === bPrefix) { + const suffix = key.substring(bPrefix.length) + const newKey = aPrefix + suffix + copyField(state.fields, key, newFields, newKey) + } else { + // Keep this field that does not match the name + newFields[key] = state.fields[key] + } + }) - restoreFunctions(state, backupState) + state.fields = newFields } export default swap diff --git a/src/swap.test.js b/src/swap.test.js index 013940a..b74dd7b 100644 --- a/src/swap.test.js +++ b/src/swap.test.js @@ -93,10 +93,10 @@ describe('swap', () => { } }, fields: { - 'foo[0]': { - name: 'foo[0]', - touched: false, - error: 'Error C', + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'Error A', lastFieldState: undefined }, 'foo[1]': { @@ -105,10 +105,10 @@ describe('swap', () => { error: 'Error B', lastFieldState: 'anything' // unchanged }, - 'foo[2]': { - name: 'foo[2]', - touched: true, - error: 'Error A', + 'foo[0]': { + name: 'foo[0]', + touched: false, + error: 'Error C', lastFieldState: undefined }, 'foo[3]': { @@ -215,22 +215,22 @@ describe('swap', () => { } }, fields: { - 'foo[0].dog': { - name: 'foo[0].dog', + 'foo[2].dog': { + name: 'foo[2].dog', touched: true, - error: 'Error C Dog', + error: 'Error A Dog', lastFieldState: undefined }, - 'foo[0].cat': { - name: 'foo[0].cat', + 'foo[2].cat': { + name: 'foo[2].cat', touched: false, - error: 'Error C Cat', + error: 'Error A Cat', lastFieldState: undefined }, - 'foo[0].axe': { - name: 'foo[0].axe', + 'foo[2].rock': { + name: 'foo[2].rock', touched: false, - error: 'Error C Axe', + error: 'Error A Rock', lastFieldState: undefined }, 'foo[1].dog': { @@ -245,22 +245,22 @@ describe('swap', () => { error: 'Error B Cat', lastFieldState: 'anything' // unchanged }, - 'foo[2].dog': { - name: 'foo[2].dog', + 'foo[0].dog': { + name: 'foo[0].dog', touched: true, - error: 'Error A Dog', + error: 'Error C Dog', lastFieldState: undefined }, - 'foo[2].cat': { - name: 'foo[2].cat', + 'foo[0].cat': { + name: 'foo[0].cat', touched: false, - error: 'Error A Cat', + error: 'Error C Cat', lastFieldState: undefined }, - 'foo[2].rock': { - name: 'foo[2].rock', + 'foo[0].axe': { + name: 'foo[0].axe', touched: false, - error: 'Error A Rock', + error: 'Error C Axe', lastFieldState: undefined }, 'foo[3].dog': {