diff --git a/README.md b/README.md index 38d6507..278b839 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ const customer = form.mutators.pop('customers') - [Mutators](#mutators) + - [`form.mutators.concat(name: string, value: Array) => void`](#formmutatorsconcatname-string-value-arrayany--void) - [`form.mutators.insert(name: string, index: number, value: any) => undefined`](#formmutatorsinsertname-string-index-number-value-any--undefined) - - [`form.mutators.merge(name: string, value: Array) => void`](#formmutatorsmergename-string-value-arrayany--void) - [`form.mutators.move(name: string, from: number, to: number) => undefined`](#formmutatorsmovename-string-from-number-to-number--undefined) - [`form.mutators.pop(name: string) => any`](#formmutatorspopname-string--any) - [`form.mutators.push(name: string, value: any) => void`](#formmutatorspushname-string-value-any--void) @@ -65,13 +65,13 @@ const customer = form.mutators.pop('customers') ## Mutators -### `form.mutators.insert(name: string, index: number, value: any) => undefined` +### `form.mutators.concat(name: string, value: Array) => void` -Inserts a value into the specified index of the array field. +Concatenates an array at the end of the array field. -### `form.mutators.merge(name: string, value: Array) => void` +### `form.mutators.insert(name: string, index: number, value: any) => undefined` -Merges an array at the end of the array field. +Inserts a value into the specified index of the array field. ### `form.mutators.move(name: string, from: number, to: number) => undefined` diff --git a/src/merge.js b/src/concat.js similarity index 84% rename from src/merge.js rename to src/concat.js index f3e80bb..3aa0769 100644 --- a/src/merge.js +++ b/src/concat.js @@ -1,7 +1,7 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' -const merge: Mutator = ( +const concat: Mutator = ( [name, value]: any[], state: MutableState, { changeValue }: Tools @@ -13,4 +13,4 @@ const merge: Mutator = ( ) } -export default merge +export default concat diff --git a/src/merge.test.js b/src/concat.test.js similarity index 78% rename from src/merge.test.js rename to src/concat.test.js index e1ac21a..1705d06 100644 --- a/src/merge.test.js +++ b/src/concat.test.js @@ -1,16 +1,16 @@ -import merge from './merge' +import concat from './concat' -describe('merge', () => { +describe('concat', () => { const getOp = value => { const changeValue = jest.fn() - merge(['foo', value], {}, { changeValue }) + concat(['foo', value], {}, { changeValue }) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { const changeValue = jest.fn() const state = {} - const result = merge(['foo', ['bar', 'baz']], state, { changeValue }) + const result = concat(['foo', ['bar', 'baz']], state, { changeValue }) expect(result).toBeUndefined() expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) @@ -26,7 +26,7 @@ describe('merge', () => { expect(result).toEqual(['bar', 'baz']) }) - it('should merge the array at the end of the original array', () => { + it('should concat the array at the end of the original array', () => { const op = getOp(['d', 'e']) const result = op(['a', 'b', 'c']) expect(Array.isArray(result)).toBe(true) diff --git a/src/index.d.test.ts b/src/index.d.test.ts index 22b3e54..ea48168 100644 --- a/src/index.d.test.ts +++ b/src/index.d.test.ts @@ -15,7 +15,10 @@ const form = createForm({ const mutators: Mutators = (form.mutators as any) as Mutators mutators.insert('customers', 0, { firstName: '', lastName: '' }) -mutators.merge('customers', [{ firstName: '', lastName: '' }, { firstName: '', lastName: '' }]) +mutators.concat('customers', [ + { firstName: '', lastName: '' }, + { firstName: '', lastName: '' } +]) mutators.move('customers', 0, 1) const customer = mutators.pop('customers') mutators.push('customers', { firstName: '', lastName: '' }) diff --git a/src/index.d.ts b/src/index.d.ts index 8c36276..e76e7fd 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,7 +1,7 @@ import { Mutator } from 'final-form' export const insert: Mutator -export const merge: Mutator +export const concat: Mutator export const move: Mutator export const pop: Mutator export const push: Mutator @@ -14,7 +14,7 @@ export const unshift: Mutator export interface DefaultType { insert: Mutator - merge: Mutator + concat: Mutator move: Mutator pop: Mutator push: Mutator @@ -32,7 +32,7 @@ export default d /** The shape of the mutators once final-form has bound them to state */ export interface Mutators { insert: (name: string, index: number, value: any) => void - merge: (name: string, value: Array) => void, + concat: (name: string, value: Array) => void move: (name: string, from: number, to: number) => void pop: (name: string) => any push: (name: string, value: any) => void diff --git a/src/index.js b/src/index.js index c144fd9..bdc0fd9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ // @flow import type { Mutator } from 'final-form' import insert from './insert' -import merge from './merge' +import concat from './concat' import move from './move' import pop from './pop' import push from './push' @@ -14,7 +14,7 @@ import update from './update' const mutators: { [string]: Mutator } = { insert, - merge, + concat, move, pop, push, diff --git a/src/index.js.flow b/src/index.js.flow index 46666c8..6a70ddb 100644 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -8,7 +8,7 @@ declare export default DefaultType /** The shape of the mutators once final-form has bound them to state */ export type Mutators = { insert: (name: string, index: number, value: any) => void, - merge: (name: string, value: Array) => void, + concat: (name: string, value: Array) => void, move: (name: string, from: number, to: number) => void, pop: (name: string) => any, push: (name: string, value: any) => void, diff --git a/src/insert.test.js b/src/insert.test.js index 864bd1e..b7574a7 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -32,7 +32,8 @@ describe('insert', () => { const state = { formState: { values: { - foo: ['one', 'two'] + foo: ['one', 'two'], + anotherField: 42 } }, fields: { @@ -43,8 +44,11 @@ describe('insert', () => { }, 'foo[1]': { name: 'foo[1]', - touched: false, - error: 'Second Error' + touched: false + }, + anotherField: { + name: 'anotherField', + touched: true } } } diff --git a/src/remove.test.js b/src/remove.test.js index 344df5b..49e6d3d 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -61,7 +61,8 @@ describe('remove', () => { const state = { formState: { values: { - foo: array + foo: array, + anotherField: 42 } }, fields: { @@ -84,6 +85,10 @@ describe('remove', () => { name: 'foo[3]', touched: false, error: 'D Error' + }, + anotherField: { + name: 'anotherField', + touched: false } } } @@ -93,7 +98,8 @@ describe('remove', () => { expect(state).toEqual({ formState: { values: { - foo: ['a', 'c', 'd'] + foo: ['a', 'c', 'd'], + anotherField: 42 } }, fields: { @@ -111,6 +117,10 @@ describe('remove', () => { name: 'foo[2]', touched: false, error: 'D Error' + }, + anotherField: { + name: 'anotherField', + touched: false } } }) diff --git a/src/removeBatch.js b/src/removeBatch.js index 1c487a5..6382a1f 100644 --- a/src/removeBatch.js +++ b/src/removeBatch.js @@ -1,36 +1,66 @@ // @flow import type { MutableState, Mutator, Tools } from 'final-form' +const countBelow = (array, value) => + array.reduce((count, item) => (item < value ? count + 1 : count), 0) + const removeBatch: Mutator = ( [name, indexes]: any[], state: MutableState, { changeValue }: Tools ) => { + 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) + } + } + + let returnValue = [] changeValue( state, name, (array: ?(any[])): ?(any[]) => { - if (!array || !indexes) { + // use original order of indexes for return value + returnValue = indexes.map(index => array && array[index]) + if (!array || !sortedIndexes.length) { return array } - let mask = new Array(indexes.length) - for (let i = 0; i < indexes.length; i++) { - mask[indexes[i]] = true - } + const copy = [...array] + const removed = [] + sortedIndexes.forEach((index: number) => { + copy.splice(index - removed.length, 1) + removed.push(array && array[index]) + }) + return copy + } + ) - let offset = 0 - for (let i = 0; i < array.length; i++) { - if (mask[i] === undefined) { - array[offset] = array[i] - offset++ - } + // now we have to remove any subfields for our indexes, + // and decrement all higher indexes. + const pattern = new RegExp(`^${name}\\[(\\d+)\\](.*)`) + 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]}` + newFields[decrementedKey] = state.fields[key] + newFields[decrementedKey].name = decrementedKey } - - array.length = offset - return array + } else { + newFields[key] = state.fields[key] } - ) + }) + state.fields = newFields + return returnValue } export default removeBatch diff --git a/src/removeBatch.test.js b/src/removeBatch.test.js index 0aff698..a6d5912 100644 --- a/src/removeBatch.test.js +++ b/src/removeBatch.test.js @@ -1,22 +1,140 @@ import removeBatch from './removeBatch' +import { getIn, setIn } from 'final-form' describe('merge', () => { const getOp = value => { const changeValue = jest.fn() - removeBatch(['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' + } + } + } + removeBatch(['foo', value], state, { changeValue }) return changeValue.mock.calls[0][2] } it('should call changeValue once', () => { - const changeValue = jest.fn() - const state = {} + // 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', 'three'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'Third Error' + } + } + } const result = removeBatch(['foo', [1, 2]], state, { changeValue }) - expect(result).toBeUndefined() + expect(Array.isArray(result)).toBe(true) + expect(result).toEqual(['two', 'three']) + expect(changeValue).toHaveBeenCalled() + expect(changeValue).toHaveBeenCalledTimes(1) + expect(changeValue.mock.calls[0][0]).toBe(state) + expect(changeValue.mock.calls[0][1]).toBe('foo') + expect(typeof changeValue.mock.calls[0][2]).toBe('function') + expect(state).toEqual({ + formState: { + values: { + foo: ['one'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + } + } + }) + }) + + it('should not matter if indexes are out of order', () => { + // 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', 'three'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: true, + error: 'First Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'Second Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'Third Error' + } + } + } + const result = removeBatch(['foo', [2, 0]], state, { changeValue }) + expect(Array.isArray(result)).toBe(true) + expect(result).toEqual(['three', 'one']) expect(changeValue).toHaveBeenCalled() expect(changeValue).toHaveBeenCalledTimes(1) expect(changeValue.mock.calls[0][0]).toBe(state) expect(changeValue.mock.calls[0][1]).toBe('foo') expect(typeof changeValue.mock.calls[0][2]).toBe('function') + expect(state).toEqual({ + formState: { + values: { + foo: ['two'] + } + }, + fields: { + 'foo[0]': { + name: 'foo[0]', + touched: false, + error: 'Second Error' + } + } + }) }) it('should return undefined if array is undefined', () => { @@ -38,4 +156,92 @@ describe('merge', () => { expect(Array.isArray(result)).toBe(true) expect(result).toEqual(['a', 'c', 'e']) }) + + it('should ignore duplicate indexes', () => { + const op = getOp([1, 1, 1, 3]) + const result = op(['a', 'b', 'c', 'd', 'e']) + expect(Array.isArray(result)).toBe(true) + expect(result).toEqual(['a', 'c', 'e']) + }) + + it('should adjust higher indexes when removing', () => { + 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) || {} + } + const state = { + formState: { + values: { + foo: array, + anotherField: 42 + } + }, + 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' + }, + 'foo[4]': { + name: 'foo[4]', + touched: true, + error: 'E Error' + }, + anotherField: { + name: 'anotherField', + touched: false + } + } + } + const returnValue = removeBatch(['foo', [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]': { + name: 'foo[0]', + touched: true, + error: 'A Error' + }, + 'foo[1]': { + name: 'foo[1]', + touched: false, + error: 'D Error' + }, + 'foo[2]': { + name: 'foo[2]', + touched: true, + error: 'E Error' + }, + anotherField: { + name: 'anotherField', + touched: false + } + } + }) + }) })