diff --git a/src/insert.js b/src/insert.js index b759812..7636cee 100644 --- a/src/insert.js +++ b/src/insert.js @@ -1,6 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' import moveFieldState from './moveFieldState' +import { escapeRegexTokens } from './utils' const insert: Mutator = ( [name, index, value]: any[], @@ -16,7 +17,7 @@ const insert: Mutator = ( const backup = { ...state.fields } // now we have increment any higher indexes - const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) + const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) // we need to increment high indices first so // lower indices won't overlap diff --git a/src/insert.test.js b/src/insert.test.js index b432816..d35d094 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -162,4 +162,84 @@ describe('insert', () => { } }) }) + + it('should increment other field data from the specified index (nested arrays)', () => { + const array = ['a', 'b', 'c', 'd'] + // 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 resetFieldState = name => { + state.fields[name].touched = false + } + const state = { + formState: { + values: { + foo: [array] + } + }, + fields: { + 'foo[0][0]': { + name: 'foo[0][0]', + touched: true, + error: 'A Error' + }, + 'foo[0][1]': { + name: 'foo[0][1]', + touched: true, + error: 'B Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + touched: true, + error: 'C Error' + }, + 'foo[0][3]': { + name: 'foo[0][3]', + touched: false, + error: 'D Error' + } + } + } + const returnValue = insert(['foo[0]', 1, 'NEWVALUE'], state, { + changeValue, + resetFieldState + }) + expect(returnValue).toBeUndefined() + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: [['a', 'NEWVALUE', 'b', 'c', 'd']] + } + }, + fields: { + 'foo[0][0]': { + name: 'foo[0][0]', + touched: true, + error: 'A Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + touched: true, + error: 'B Error', + lastFieldState: undefined + }, + 'foo[0][3]': { + name: 'foo[0][3]', + touched: true, + error: 'C Error', + lastFieldState: undefined + }, + 'foo[0][4]': { + name: 'foo[0][4]', + touched: false, + error: 'D Error', + lastFieldState: undefined + } + } + }) + }) }) diff --git a/src/pop.js b/src/pop.js index a135c22..57fe265 100644 --- a/src/pop.js +++ b/src/pop.js @@ -1,5 +1,6 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' +import { escapeRegexTokens } from './utils' const pop: Mutator = ( [name]: any[], @@ -21,7 +22,9 @@ const pop: Mutator = ( // now we have to remove any subfields for our index, if (removedIndex !== undefined) { - const pattern = new RegExp(`^${name}\\[${removedIndex}].*`) + const pattern = new RegExp( + `^${escapeRegexTokens(name)}\\[${removedIndex}].*` + ) Object.keys(state.fields).forEach(key => { if (pattern.test(key)) { delete state.fields[key] diff --git a/src/pop.test.js b/src/pop.test.js index af4b363..f1a8b77 100644 --- a/src/pop.test.js +++ b/src/pop.test.js @@ -195,4 +195,81 @@ describe('pop', () => { } }) }) + + it('should pop value off the end of array and return it (nested arrays)', () => { + const array = ['a', 'b', 'c', 'd'] + // 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: [array], + anotherField: 42 + } + }, + fields: { + 'foo[0][0]': { + name: 'foo[0][0]', + touched: true, + error: 'A Error' + }, + 'foo[0][1]': { + name: 'foo[0][1]', + touched: false, + error: 'B Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + touched: true, + error: 'C Error' + }, + 'foo[0][3]': { + name: 'foo[0][3]', + touched: false, + error: 'D Error' + }, + anotherField: { + name: 'anotherField', + touched: false + } + } + } + const returnValue = pop(['foo[0]'], state, { changeValue }) + expect(returnValue).toBe('d') + expect(Array.isArray(state.formState.values.foo)).toBe(true) + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: [['a', 'b', 'c']], + anotherField: 42 + } + }, + fields: { + 'foo[0][0]': { + name: 'foo[0][0]', + touched: true, + error: 'A Error' + }, + 'foo[0][1]': { + name: 'foo[0][1]', + touched: false, + error: 'B Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + touched: true, + error: 'C Error' + }, + anotherField: { + name: 'anotherField', + touched: false + } + } + }) + }) }) diff --git a/src/remove.js b/src/remove.js index a293d9b..8af1f60 100644 --- a/src/remove.js +++ b/src/remove.js @@ -1,6 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' import moveFieldState from './moveFieldState' +import { escapeRegexTokens } from './utils' const remove: Mutator = ( [name, index]: any[], @@ -17,7 +18,7 @@ const remove: Mutator = ( // now we have to remove any subfields for our index, // and decrement all higher indexes. - const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) + const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) const backup = { ...state, fields: { ...state.fields } } Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) diff --git a/src/remove.test.js b/src/remove.test.js index 4b92adc..fd4a685 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -160,6 +160,118 @@ describe('remove', () => { } }) }) + + + it('should remove value from the specified index, and return it (nested arrays)', () => { + const array = ['a', 'b', 'c', 'd'] + // 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) || {} + } + function blur0() {} + function change0() {} + function focus0() {} + function blur1() {} + function change1() {} + function focus1() {} + function blur2() {} + function change2() {} + function focus2() {} + function blur3() {} + function change3() {} + function focus3() {} + const state = { + formState: { + values: { + foo: [array], + anotherField: 42 + } + }, + 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: false, + error: 'B Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + blur: blur2, + change: change2, + focus: focus2, + 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 + } + } + } + const returnValue = remove(['foo[0]', 1], state, { changeValue }) + expect(returnValue).toBe('b') + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: [['a', 'c', 'd']], + anotherField: 42 + } + }, + 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, + change: change2, + focus: focus2, + touched: false, + 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' }] diff --git a/src/removeBatch.js b/src/removeBatch.js index 80f836e..8c2480e 100644 --- a/src/removeBatch.js +++ b/src/removeBatch.js @@ -1,6 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' import moveFieldState from './moveFieldState' +import { escapeRegexTokens } from './utils' const countBelow = (array, value) => array.reduce((count, item) => (item < value ? count + 1 : count), 0) @@ -38,7 +39,7 @@ const removeBatch: Mutator = ( // now we have to remove any subfields for our indexes, // and decrement all higher indexes. - const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) + const pattern = new RegExp(`^${escapeRegexTokens(name)}\\[(\\d+)\\](.*)`) const newState = { ...state, fields: {} } Object.keys(state.fields).forEach(key => { const tokens = pattern.exec(key) diff --git a/src/removeBatch.test.js b/src/removeBatch.test.js index 81741ea..15f0c8d 100644 --- a/src/removeBatch.test.js +++ b/src/removeBatch.test.js @@ -330,4 +330,129 @@ describe('removeBatch', () => { } }) }) + + it('should adjust higher indexes when removing (nested arrays)', () => { + const array = ['a', 'b', 'c', 'd', 'e'] + // 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) || {} + } + function blur0() {} + function blur1() {} + function blur2() {} + function blur3() {} + function blur4() {} + function change0() {} + function change1() {} + function change2() {} + function change3() {} + function change4() {} + function focus0() {} + function focus1() {} + function focus2() {} + function focus3() {} + function focus4() {} + const state = { + formState: { + values: { + foo: [array], + anotherField: 42 + } + }, + 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: false, + error: 'B Error' + }, + 'foo[0][2]': { + name: 'foo[0][2]', + blur: blur2, + change: change2, + focus: focus2, + touched: true, + error: 'C Error' + }, + 'foo[0][3]': { + name: 'foo[0][3]', + blur: blur3, + change: change3, + focus: focus3, + 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, { + changeValue + }) + expect(returnValue).toEqual(['b', 'c']) + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: [['a', 'd', 'e']], + anotherField: 42 + } + }, + fields: { + 'foo[0][0]': { + name: 'foo[0][0]', + blur: blur0, + change: change0, + focus: focus0, + touched: true, + error: 'A Error', + lastFieldState: undefined + }, + 'foo[0][1]': { + name: 'foo[0][1]', + 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', + lastFieldState: undefined + }, + anotherField: { + name: 'anotherField', + touched: false + } + } + }) + }) }) diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..35c90cb --- /dev/null +++ b/src/utils.js @@ -0,0 +1,6 @@ +// @flow + +// From MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping +export const escapeRegexTokens = (string: string): string => + string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +