From 3a9566ac1be540e867249d323b26aadab5c50159 Mon Sep 17 00:00:00 2001 From: Erik Rasmussen Date: Fri, 12 Jul 2019 12:20:31 -0400 Subject: [PATCH] Smarter field state management during insert and remove --- src/insert.js | 20 ++++++ src/insert.test.js | 113 +++++++++++++++++++++++++++++++++- src/remove.js | 21 +++++++ src/remove.test.js | 100 +++++++++++++++++++++++++++--- src/shift.js | 24 +------- src/shift.test.js | 146 ++++++++++++++++++++++++++++++++++++++++---- src/unshift.js | 11 +--- src/unshift.test.js | 104 +++++++++++++++++++++++++++++-- 8 files changed, 479 insertions(+), 60 deletions(-) diff --git a/src/insert.js b/src/insert.js index 85275d2..0a77dee 100644 --- a/src/insert.js +++ b/src/insert.js @@ -15,6 +15,26 @@ const insert: Mutator = ( return copy } ) + + // now we have increment any higher indexes + const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) + const changes = {} + 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]}` + changes[incrementedKey] = state.fields[key] + changes[incrementedKey].name = incrementedKey + } + if (fieldIndex === index) { + delete state.fields[key] + } + } + }) + state.fields = { ...state.fields, ...changes } } export default insert diff --git a/src/insert.test.js b/src/insert.test.js index 3333551..864bd1e 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -1,15 +1,53 @@ import insert from './insert' +import { getIn, setIn } from 'final-form' describe('insert', () => { const getOp = (index, value) => { const changeValue = jest.fn() - insert(['foo', index, value], {}, { changeValue }) + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } + insert(['foo', index, value], state, { changeValue }) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } const result = insert(['foo', 0, 'bar'], state, { changeValue }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() @@ -35,4 +73,75 @@ describe('insert', () => { expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'd', 'b', 'c']) }) + + it('should increment other field data from the specified index', () => { + 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 + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'B Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'C Error' + }, + 'foo[3]': { + name: 'foo[3]', + touched: false, + error: 'D Error' + } + } + } + const returnValue = insert(['foo', 1, 'NEWVALUE'], state, { changeValue }) + 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]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: false, + error: 'B Error' + }, + 'foo[3]': { + name: 'foo[3]', + touched: true, + error: 'C Error' + }, + 'foo[4]': { + name: 'foo[4]', + touched: false, + error: 'D Error' + } + } + }) + }) }) diff --git a/src/remove.js b/src/remove.js index 8301dcb..a05846f 100644 --- a/src/remove.js +++ b/src/remove.js @@ -17,6 +17,27 @@ const remove: Mutator = ( return copy } ) + + // now we have to remove any subfields for our index, + // and decrement all 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) { + // delete any subfields for this array item + delete state.fields[key] + } else if (fieldIndex > index) { + // shift all higher ones down + delete state.fields[key] + const decrementedKey = `${name}[${fieldIndex - 1}]${tokens[2]}` + state.fields[decrementedKey] = backup[key] + state.fields[decrementedKey].name = decrementedKey + } + } + }) return returnValue } diff --git a/src/remove.test.js b/src/remove.test.js index f7bebc5..344df5b 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -1,9 +1,28 @@ import remove from './remove' +import { getIn, setIn } from 'final-form' describe('remove', () => { it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } const result = remove(['foo', 0], state, { changeValue }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() @@ -15,7 +34,15 @@ describe('remove', () => { it('should treat undefined like an empty array', () => { const changeValue = jest.fn() - const returnValue = remove(['foo', 1], {}, { changeValue }) + const state = { + formState: { + values: { + foo: undefined + } + }, + fields: {} + } + const returnValue = remove(['foo', 1], state, { changeValue }) expect(returnValue).toBeUndefined() const op = changeValue.mock.calls[0][2] const result = op(undefined) @@ -25,14 +52,67 @@ describe('remove', () => { it('should remove value from the specified index, and return it', () => { const array = ['a', 'b', 'c', 'd'] - let result - const changeValue = jest.fn((args, state, op) => { - result = op(array) - }) - const returnValue = remove(['foo', 1], {}, { changeValue }) + // 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 + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'B Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'C Error' + }, + 'foo[3]': { + name: 'foo[3]', + touched: false, + error: 'D Error' + } + } + } + const returnValue = remove(['foo', 1], state, { changeValue }) expect(returnValue).toBe('b') - expect(result).not.toBe(array) // copied - expect(Array.isArray(result)).toBe(true) - expect(result).toEqual(['a', 'c', 'd']) + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: ['a', 'c', 'd'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: true, + error: 'C Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: false, + error: 'D Error' + } + } + }) }) }) diff --git a/src/shift.js b/src/shift.js index e05fd27..3f963f1 100644 --- a/src/shift.js +++ b/src/shift.js @@ -1,26 +1,8 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' +import remove from './remove' -const shift: Mutator = ( - [name]: any[], - state: MutableState, - { changeValue }: Tools -) => { - let result - changeValue( - state, - name, - (array: ?(any[])): ?(any[]) => { - if (array) { - if (!array.length) { - return [] - } - result = array[0] - return array.slice(1, array.length) - } - } - ) - return result -} +const shift: Mutator = ([name]: any[], state: MutableState, tools: Tools) => + remove([name, 0], state, tools) export default shift diff --git a/src/shift.test.js b/src/shift.test.js index cbf5b01..ea297d8 100644 --- a/src/shift.test.js +++ b/src/shift.test.js @@ -1,9 +1,28 @@ import shift from './shift' +import { getIn, setIn } from 'final-form' describe('shift', () => { it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } const result = shift(['foo'], state, { changeValue }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() @@ -13,18 +32,32 @@ describe('shift', () => { expect(typeof changeValue.mock.calls[0][2]).toBe('function') }) - it('should return undefined if array is undefined', () => { + it('should treat undefined like an empty array', () => { const changeValue = jest.fn() - const returnValue = shift(['foo'], {}, { changeValue }) + const state = { + formState: { + values: {} + }, + fields: {} + } + const returnValue = shift(['foo'], state, { changeValue }) const op = changeValue.mock.calls[0][2] expect(returnValue).toBeUndefined() const result = op(undefined) - expect(result).toBeUndefined() + expect(result).toEqual([]) }) it('should return empty array if array is empty', () => { const changeValue = jest.fn() - const returnValue = shift(['foo'], {}, { changeValue }) + const state = { + formState: { + values: { + foo: [] + } + }, + fields: {} + } + const returnValue = shift(['foo'], state, { changeValue }) const op = changeValue.mock.calls[0][2] expect(returnValue).toBeUndefined() const result = op([]) @@ -34,14 +67,101 @@ describe('shift', () => { it('should remove value from start of array, and return it', () => { const array = ['a', 'b', 'c'] - let result - const changeValue = jest.fn((args, state, op) => { - result = op(array) - }) - const returnValue = shift(['foo'], {}, { changeValue }) - expect(result).not.toBe(array) // copied + // 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 + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'B Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'C Error' + } + } + } + const returnValue = shift(['foo'], state, { changeValue }) + expect(state.formState.values.foo).not.toBe(array) // copied expect(returnValue).toBe('a') - expect(Array.isArray(result)).toBe(true) - expect(result).toEqual(['b', 'c']) + expect(Array.isArray(state.formState.values.foo)).toBe(true) + expect(state).toEqual({ + formState: { + values: { + foo: ['b', 'c'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: false, + error: 'B Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: true, + error: 'C Error' + } + } + }) + }) + + it('should shift field state, too', () => { + // 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'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } + shift(['foo'], state, { changeValue }) + expect(state).toEqual({ + formState: { + values: { + foo: ['two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: false, + error: 'Second Error' + } + } + }) }) }) diff --git a/src/unshift.js b/src/unshift.js index fbbd33d..dd3cad9 100644 --- a/src/unshift.js +++ b/src/unshift.js @@ -1,16 +1,11 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' +import insert from './insert' const unshift: Mutator = ( [name, value]: any[], state: MutableState, - { changeValue }: Tools -) => { - changeValue( - state, - name, - (array: ?(any[])): any[] => (array ? [value, ...array] : [value]) - ) -} + tools: Tools +) => insert([name, 0, value], state, tools) export default unshift diff --git a/src/unshift.test.js b/src/unshift.test.js index 8b3d074..5038bc3 100644 --- a/src/unshift.test.js +++ b/src/unshift.test.js @@ -1,15 +1,53 @@ import unshift from './unshift' +import { getIn, setIn } from 'final-form' describe('unshift', () => { const getOp = value => { const changeValue = jest.fn() - unshift(['foo', value], {}, { changeValue }) + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } + unshift(['foo', value], state, { changeValue }) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() - const state = {} + const state = { + formState: { + values: { + foo: ['one', 'two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + } + } + } const result = unshift(['foo', 'bar'], state, { changeValue }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() @@ -28,9 +66,63 @@ describe('unshift', () => { }) it('should insert value to beginning of array', () => { - const op = getOp('d') - const result = op(['a', 'b', 'c']) - expect(Array.isArray(result)).toBe(true) - expect(result).toEqual(['d', 'a', 'b', 'c']) + const array = ['a', 'b', 'c'] + // 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 + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'B Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'C Error' + } + } + } + const returnValue = unshift(['foo', 'NEWVALUE'], state, { changeValue }) + expect(returnValue).toBeUndefined() + expect(state.formState.values.foo).not.toBe(array) // copied + expect(state).toEqual({ + formState: { + values: { + foo: ['NEWVALUE', 'a', 'b', 'c'] + } + }, + fields: { + 'foo[1]': { + name: 'foo[1]', + touched: true, + error: 'A Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: false, + error: 'B Error' + }, + 'foo[3]': { + name: 'foo[3]', + touched: true, + error: 'C Error' + } + } + }) }) })